@kitn.ai/ui 0.26.0 → 0.27.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 +16 -5
- 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-BnFKqqtY.js} +1 -1
- package/dist/action-icons-CBskfBMz.js +1 -0
- package/dist/arrow-down-Br0qtpsz.js +1 -0
- package/dist/arrow-left-Dj7xgE12.js +1 -0
- package/dist/artifact-CvXEm1lS.js +1 -0
- package/dist/assets/dev-5XzXYR21.js +91 -0
- package/dist/attachments-C43ZegEq.js +1 -0
- package/dist/{elements/chunks/audio-visualizer-D7lB6mvR.js → audio-visualizer-B-79y1Tj.js} +1 -1
- package/dist/badge-BpUbq8c0.js +1 -0
- package/dist/bash-InADTalH.js +1 -6
- package/dist/bell-sn17Ip9v.js +1 -0
- package/dist/{elements/chunks/button-flUbeufF.js → button-BH6rN0U-.js} +1 -1
- package/dist/card-renderer-Cb1gQmth.js +1 -0
- package/dist/card-routing-DifQgS7n.js +1 -0
- package/dist/check-DPeeMUYx.js +1 -0
- package/dist/checkbox-Dc9Yov_B.js +1 -0
- package/dist/checkbox-group-B1kDLLYk.js +1 -0
- package/dist/chevron-down-CHSWk7ZT.js +1 -0
- package/dist/chevron-right-B7l_fMgx.js +1 -0
- package/dist/choice-card-BT3c1S8g.js +1 -0
- package/dist/circle-Bb0yOpqM.js +1 -0
- package/dist/circle-check-BGZpCiz7.js +1 -0
- package/dist/circle-x-wobWkoSx.js +1 -0
- package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
- package/dist/{elements/chunks/code-block-BdLiTitd.js → code-block-CUQ5mtv0.js} +1 -1
- package/dist/{elements/chunks/collapsible-BzbXpPzr.js → collapsible-CPJbf6FM.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 +46 -1
- package/dist/components/conversation-item.d.ts +19 -7
- package/dist/components/conversation-list.d.ts +17 -5
- package/dist/components/form-widgets.d.ts +67 -2
- package/dist/components/form.d.ts +48 -1
- 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/composer-CjngLzal.js +106 -0
- package/dist/confirm-card-DCx_A4bU.js +1 -0
- package/dist/construct-cli.es.js +1169 -0
- package/dist/{elements/chunks/context-VLIL3sJ-.js → context-BY8V-Nlp.js} +1 -1
- package/dist/controllable-D43BKmtX.js +1 -0
- package/dist/conversation-list-D4lLU_yS.js +1 -0
- package/dist/copy-B6DO4PqB.js +1 -0
- package/dist/core-AYMC6_lb.js +12 -5874
- package/dist/{create-tween-CQED4T0g.js → create-tween-BPzaefTp.js} +1 -1
- package/dist/{create-tween-BFT6c-Y8.js → create-tween-CnMHviKB.js} +1 -1
- package/dist/{create-tween-CJNlzoaO.js → create-tween-D1aiGZZh.js} +1 -1
- package/dist/{elements/chunks/create-tween-c4Z8TDYz.js → create-tween-DUSQUKpr.js} +2 -2
- package/dist/css-M7EaDHN_.js +1 -6
- package/dist/custom-elements.json +1400 -218
- package/dist/default-input-CucQEOqH.js +1 -0
- package/dist/define-CGGX-7Vg.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-CbLtajUQ.js} +1 -1
- package/dist/{elements/chunks/dropdown-BgWTthUx.js → dropdown-BwhViJFd.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 +101 -7
- 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 +362 -24
- package/dist/{elements/chunks/ellipsis-7gU33mL2.js → ellipsis-DFr4Vqr3.js} +1 -1
- package/dist/embed-5s_vU4J0.js +1 -0
- package/dist/engine-javascript-vq0WuIJl.js +14 -2516
- package/dist/{elements/chunks/external-link-CSOEsNlx.js → external-link-u9d85ORQ.js} +1 -1
- package/dist/file-text-CX7_x42o.js +1 -0
- package/dist/{elements/chunks/file-tree-zvB2i7P9.js → file-tree-D3kBjXI5.js} +1 -1
- package/dist/folder-V-YdIL8f.js +1 -0
- package/dist/form-BRNUMCN2.js +1 -0
- package/dist/github-dark-dimmed-DUshB20C.js +1 -4
- package/dist/github-light-JYsPkUQd.js +1 -4
- package/dist/hover-card-DaeiYrTA.js +1 -0
- package/dist/html-CPZ3oZQ7.js +1 -10
- package/dist/icon-BpixdZNq.js +1 -0
- package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7330 -5852
- package/dist/index.server.js +5823 -4759
- package/dist/info-8mFsIjS3.js +1 -0
- package/dist/input-d1Yuu-hz.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-961zMPFB.js +1 -0
- package/dist/link-DlZZBGaR.js +1 -0
- package/dist/{elements/chunks/link-preview-OQ9IEq0f.js → link-preview-DIIbV2Sd.js} +1 -1
- package/dist/loader-YQszHFaW.js +1 -0
- package/dist/{elements/chunks/markdown-DgBFaGAz.js → markdown-CVk1MPsj.js} +1 -1
- package/dist/mcp.es.js +1285 -79
- package/dist/message-CwKVEdZg.js +1 -0
- package/dist/message-DS8lzHc1.js +1 -0
- package/dist/message-circle-Xf8Q-vZp.js +1 -0
- package/dist/message-square-Cxnwgkns.js +1 -0
- package/dist/{elements/chunks/minimize-2-DPIUKTrT.js → minimize-2-UEZmP2QT.js} +1 -1
- package/dist/{elements/chunks/model-switcher-DsT8tFB0.js → model-switcher-Kk1jEQ5t.js} +1 -1
- package/dist/{elements/chunks/overlay-DuiwdXjm.js → overlay-C1od6LOz.js} +1 -1
- package/dist/panel-right-ryET0yyn.js +1 -0
- package/dist/paperclip-BDNoZFJP.js +1 -0
- package/dist/play-OFIvYXUQ.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/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-BvYX0aoy.js +1 -0
- package/dist/prompt-suggestion-BRFu2lcu.js +1 -0
- package/dist/radio-DcL23cNJ.js +1 -0
- package/dist/react/index.d.ts +193 -19
- package/dist/react.js +216 -180
- package/dist/reasoning-Cackq4Rh.js +1 -0
- package/dist/register-impl-C7rENvPK.js +424 -0
- package/dist/{elements/chunks/resizable-D181b4fM.js → resizable-Cp9HUYih.js} +1 -1
- package/dist/rotate-ccw-0DLRDHH5.js +1 -0
- package/dist/rotate-cw-DOB9IGcU.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-emVmWP4K.js +1 -0
- package/dist/scroll-button-CeP_gv1x.js +1 -0
- package/dist/select-CWmu00tS.js +1 -0
- package/dist/{elements/chunks/separator-CeJHQqLW.js → separator-CE6vly6z.js} +1 -1
- package/dist/{elements/chunks/settings-CqaHgJF9.js → settings-CE-78SvZ.js} +1 -1
- package/dist/{elements/chunks/settings-group-CjqjLCOn.js → settings-group-ZFWNxFQR.js} +1 -1
- package/dist/{elements/chunks/skeleton-D8akBnKS.js → skeleton-BebdP4mf.js} +1 -1
- package/dist/slider-BAKRYUlz.js +1 -0
- package/dist/{solid-BtdqNZI7.js → solid-BE5ui6sy.js} +8727 -7212
- package/dist/{solid-CnavwBye.js → solid-DFhsce3O.js} +7015 -5886
- package/dist/solid.d.ts +10 -0
- package/dist/solid.js +240 -234
- package/dist/solid.server.js +240 -234
- package/dist/source-B0FlguSx.js +1 -0
- package/dist/star-CBMZvLuN.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/{elements/chunks/store-Cjq0qwYN.js → store-7eoadydQ.js} +1 -1
- package/dist/svelte--5p79yCD.js +1 -15
- package/dist/switch-CViARZCy.js +1 -0
- package/dist/tasks-card-MjCcOhR_.js +1 -0
- package/dist/text-shimmer-DKlwRF2G.js +1 -0
- package/dist/textarea-3ay2Me2-.js +1 -0
- package/dist/theme.tokens.css +82 -13
- package/dist/{elements/chunks/thumbs-up-DX2Ijk3H.js → thumbs-up-DQcu2HMO.js} +1 -1
- package/dist/toast-store-C6yKOhbk.js +1 -0
- package/dist/{elements/chunks/tool-DKQ2D1DP.js → tool-BCYNlfKy.js} +1 -1
- package/dist/{elements/chunks/tooltip-B0G99y8w.js → tooltip-05zI4hjs.js} +1 -1
- package/dist/trash-2-BR1RxGky.js +1 -0
- package/dist/{elements/chunks/triangle-alert-F9cyUEyy.js → triangle-alert-DHohDOCy.js} +1 -1
- package/dist/tsx-B8rCNbgL.js +1 -6
- 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 +22 -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-BbGW6A_u.js +1 -0
- package/dist/url-scheme-policy-DHpoTJwB.js +1 -0
- package/dist/use-card-resolution-CJW4grFa.js +1 -0
- package/dist/{variant-aurora-B8II3-I1.js → variant-aurora-BUOVgPOB.js} +2 -2
- package/dist/{variant-aurora-CMNWOSJR.js → variant-aurora-CUsUJvFI.js} +2 -2
- package/dist/{variant-aurora-CcuDMWwQ.js → variant-aurora-CW2i4bfr.js} +1 -1
- package/dist/{elements/chunks/variant-aurora-qt6Zct8M.js → variant-aurora-xiMPty8f.js} +1 -1
- package/dist/{variant-custom-L16JLLZy.js → variant-custom-BQtOnmeO.js} +1 -1
- package/dist/{variant-custom-C714mqKl.js → variant-custom-CAm9L-E5.js} +2 -2
- package/dist/{variant-custom-BdvYC7af.js → variant-custom-CAnzzppW.js} +2 -2
- package/dist/{elements/chunks/variant-custom-tJp22_BQ.js → variant-custom-D91HjAKz.js} +1 -1
- package/dist/{variant-wave-ecwykeyL.js → variant-wave-Ca1Ls4_y.js} +2 -2
- package/dist/{variant-wave-D7ruiA1W.js → variant-wave-CxX39DKd.js} +2 -2
- package/dist/{elements/chunks/variant-wave-CaQ5ti39.js → variant-wave-DYyzIIZv.js} +1 -1
- package/dist/{variant-wave-2S4La2Ka.js → variant-wave-uY5crU_K.js} +1 -1
- package/dist/vue-BmIZj4XD.js +1 -33
- package/dist/x-C5m1JFgi.js +1 -0
- package/frameworks/react/index.tsx +198 -20
- package/llms-full.txt +1178 -21
- package/llms.txt +6 -3
- package/package.json +17 -4
- package/src/agent-tooling/catalog/invariants.ts +1 -1
- 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 +131 -0
- package/src/agent-tooling/construct/codegen.ts +1353 -0
- package/src/agent-tooling/construct/construct.v1.schema.json +250 -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 +23 -0
- package/src/agent-tooling/construct/schema.ts +371 -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 +72 -9
- 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 +33 -9
- package/src/components/conversation-list.tsx +25 -10
- 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/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/elements/attachments.tsx +3 -1
- package/src/elements/chat-workspace.tsx +1 -1
- package/src/elements/chat.tsx +22 -4
- 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 +736 -46
- package/src/elements/element-nonscalar.json +15 -0
- package/src/elements/element-types.d.ts +362 -24
- 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 +2 -1
- 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/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 +10 -0
- package/src/state/index.ts +1 -1
- package/src/state/mock.ts +92 -6
- 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 +145 -6
- 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 +84 -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/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/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
package/dist/mcp.es.js
CHANGED
|
@@ -3,34 +3,34 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { z as z$1 } from "zod";
|
|
6
|
-
import { readFileSync, readdirSync
|
|
6
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { join,
|
|
8
|
+
import { join, resolve, dirname } from "node:path";
|
|
9
9
|
var z = Object.defineProperty;
|
|
10
10
|
var M = (e, t, i) => t in e ? z(e, t, { enumerable: true, configurable: true, writable: true, value: i }) : e[t] = i;
|
|
11
|
-
var
|
|
12
|
-
const U = "https://json-schema.org/draft/2020-12/schema", F = "https://kitn.ai/schemas/card/artifact.schema.json", J = "ArtifactCardData", B = "Generated-artifact viewer payload: a framed preview plus the source files behind it. A deliberately NARROW subset of the Artifact component's props — it says WHAT to show, never how the viewer behaves. Toolbar composition, view-state and the iframe `sandbox` are host-owned and stay off the wire, so a model cannot widen its own sandbox or hide the chrome the user needs to inspect what it built.",
|
|
13
|
-
const
|
|
11
|
+
var $ = (e, t, i) => M(e, typeof t != "symbol" ? t + "" : t, i);
|
|
12
|
+
const U = "https://json-schema.org/draft/2020-12/schema", F = "https://kitn.ai/schemas/card/artifact.schema.json", J = "ArtifactCardData", B = "Generated-artifact viewer payload: a framed preview plus the source files behind it. A deliberately NARROW subset of the Artifact component's props — it says WHAT to show, never how the viewer behaves. Toolbar composition, view-state and the iframe `sandbox` are host-owned and stay off the wire, so a model cannot widen its own sandbox or hide the chrome the user needs to inspect what it built.", H = "object";
|
|
13
|
+
const Y = [{ required: ["src"] }, { required: ["files"] }], K = { src: { type: "string", format: "uri", description: "URL the preview iframe frames, and the target of the Home button. Omit for a source-only artifact (`tab: 'code'` with `files`).", "x-kai-format": "url" }, files: { type: "array", description: "Files behind the artifact: the Code tab's tree, each optionally carrying its own preview url and source.", maxItems: 500, items: { type: "object", required: ["path"], additionalProperties: false, properties: { path: { type: "string", description: "Tree label/key. Folders are built from `/`-delimited segments.", maxLength: 1024 }, url: { type: "string", format: "uri", description: "Where the preview loads this file. Defaults to `path` resolved against `src`.", "x-kai-format": "url" }, code: { type: "string", description: "Source shown in the Code tab. Omit for binary files (image/pdf), which have no code view." }, language: { type: "string", description: "Language id for syntax highlighting (e.g. 'html', 'css', 'tsx').", maxLength: 40 }, type: { type: "string", enum: ["html", "pdf", "image", "other"], description: "File kind. Drives the tree icon and whether the Code view applies; 'pdf' also routes the preview to the inline PDF viewer.", "x-kai-control": "select" }, additions: { type: "integer", minimum: 0, description: "Lines added vs the base. Rendered as a trailing '+N' stat." }, deletions: { type: "integer", minimum: 0, description: "Lines removed vs the base. Rendered as a trailing '-N' stat." }, status: { type: "string", enum: ["added", "modified", "deleted", "renamed", "untracked"], description: "Change status vs the base. Drives a trailing status letter in the conventional VCS hue.", "x-kai-control": "select" } } } }, tab: { type: "string", enum: ["preview", "code"], default: "preview", description: "Which view the card OPENS on. SEED ONLY: the user can switch freely afterwards and a revision of this card will NOT move them back, because the user's choice wins over the model's. A switch is reported back as a `state` patch.", "x-kai-control": "select" }, activeFile: { type: "string", description: "Path of the file selected in the tree when the card FIRST renders (must match a `files[].path`). Seeds the tree highlight, the Code source and the preview. SEED ONLY, like `tab`: revising this card will not move a user who has since selected a different file.", maxLength: 1024 }, displayUrl: { type: "string", description: "Friendly address shown in the path field INSTEAD of the real url, which also makes the field read-only. Use when `src` is not consumer-facing (e.g. a `data:` blob) so a clean address is shown rather than leaking it.", maxLength: 2048 }, height: { description: "Frame height. A bare number is px; a string is any CSS length. Defaults to 420px. REQUIRED CONCEPT, not decoration: the viewer is built to fill its container, and a message thread supplies no height, so the card sets one explicitly or the artifact collapses to zero and renders invisible.", oneOf: [{ type: "number", minimum: 1 }, { type: "string", maxLength: 32 }] } }, W = {
|
|
14
14
|
$schema: U,
|
|
15
15
|
$id: F,
|
|
16
16
|
title: J,
|
|
17
17
|
description: B,
|
|
18
|
-
type:
|
|
18
|
+
type: H,
|
|
19
19
|
additionalProperties: false,
|
|
20
|
-
anyOf:
|
|
20
|
+
anyOf: Y,
|
|
21
21
|
properties: K
|
|
22
|
-
},
|
|
23
|
-
$schema:
|
|
24
|
-
$id:
|
|
22
|
+
}, le = "https://json-schema.org/draft/2020-12/schema", ue = "https://kitn.ai/schemas/card/choice.schema.json", me = "ChoiceCardData", he = "Data payload for a `choice` card (CardEnvelope.data when type='choice'). A single-select 'pick one of N rich options' card; activating an option emits the `action` verb.", fe = "object", ye = ["options"], ge = { prompt: { type: "string", description: "Optional question/body text rendered above the options. Distinct from CardEnvelope.title (the card chrome heading)." }, submitLabel: { type: "string", description: "Label for the Submit button; default 'Submit'." }, options: { type: "array", minItems: 1, description: "The selectable options, rendered in order. Exactly one can be chosen.", items: { type: "object", required: ["id", "label"], properties: { id: { type: "string", minLength: 1, description: "Emitted as CardEvent.action. Must be unique within `options`. (The reserved id '__other__' is used by the allowOther escape.)", "x-kai-unique": true }, label: { type: "string", minLength: 1, description: "Visible option label." }, description: { type: "string", description: "Optional secondary line under the label." }, media: { type: "object", description: "Optional leading media: an image URL or a named icon.", properties: { image: { type: "string", description: "Image URL rendered as a leading thumbnail in the row." }, imageAlt: { type: "string", description: "Accessible alt text for `image` (decorative if omitted)." }, icon: { type: "string", description: "Named icon rendered as a leading glyph." } } }, meta: { type: "string", description: "Trailing freeform text (e.g. a price or badge), right-aligned in the row." }, recommended: { type: "boolean", default: false, description: "Renders a small 'Recommended' pill near the label." }, disabled: { type: "boolean", default: false, description: "Option is shown but not selectable, and skipped in keyboard nav." }, payload: { description: "Optional opaque payload echoed back in CardEvent.payload (any JSON)." } } } }, allowOther: { description: "Optional free-text escape ('none of these / specify'). `true` appends a final 'Other…' option; an object customizes its `label`/`placeholder`. Selecting it reveals a text input; the shared Submit button then emits `{ kind:'action', action:'__other__', payload:{ text } }`.", "x-kai-control": "allow-other" }, dismissible: { type: "boolean", default: false, description: "Show a close affordance that emits the `dismiss` verb. The card collapses to a re-openable 'dismissed' stub; the host decides via `onReopen` whether it comes back live or `expired`." } }, be = {
|
|
23
|
+
$schema: le,
|
|
24
|
+
$id: ue,
|
|
25
25
|
title: me,
|
|
26
26
|
description: he,
|
|
27
27
|
type: fe,
|
|
28
28
|
required: ye,
|
|
29
29
|
properties: ge
|
|
30
|
-
}, ve = "https://json-schema.org/draft/2020-12/schema",
|
|
30
|
+
}, ve = "https://json-schema.org/draft/2020-12/schema", we = "https://kitn.ai/schemas/card/confirm.schema.json", $e = "ConfirmCardData", xe = "Data payload for a `confirm` card (CardEnvelope.data when type='confirm').", ke = "object", Oe = ["actions"], je = { heading: { type: "string", description: "Optional in-body heading. Distinct from CardEnvelope.title (the card chrome title)." }, body: { type: "string", description: "Body text. Plain text in v1 (rendered safely; no HTML injection)." }, tone: { type: "string", enum: ["default", "warning", "danger"], default: "default", description: "Overall card tone; 'danger' adds a warning icon + accent for destructive approvals.", "x-kai-control": "tone" }, actions: { type: "array", minItems: 1, maxItems: 4, description: "The choice set. Rendered as buttons in order; max 4 keeps it a decision, not a menu.", items: { type: "object", required: ["id", "label"], properties: { id: { type: "string", minLength: 1, description: "Emitted as CardEvent.action. Must be unique within `actions`.", "x-kai-unique": true }, label: { type: "string", minLength: 1, description: "Visible button label." }, style: { type: "string", enum: ["primary", "default", "destructive"], default: "default", description: "Button emphasis. 'destructive' = red/danger; 'primary' = filled accent.", "x-kai-control": "button-style" }, payload: { description: "Optional opaque payload echoed back in CardEvent.payload (any JSON)." }, default: { type: "boolean", default: false, description: "If true, this action is the keyboard default (Enter) and gets initial focus. At most one should be true; the card uses the first if several are.", "x-kai-default-action": true } } } }, dismissible: { type: "boolean", default: false, description: "Show a close affordance that emits the `dismiss` verb." } }, Se = {
|
|
31
31
|
$schema: ve,
|
|
32
|
-
$id:
|
|
33
|
-
title:
|
|
32
|
+
$id: we,
|
|
33
|
+
title: $e,
|
|
34
34
|
description: xe,
|
|
35
35
|
type: ke,
|
|
36
36
|
required: Oe,
|
|
@@ -45,14 +45,14 @@ const H = [{ required: ["src"] }, { required: ["files"] }], K = { src: { type: "
|
|
|
45
45
|
additionalProperties: Ce,
|
|
46
46
|
properties: Re,
|
|
47
47
|
allOf: Ne
|
|
48
|
-
}, Je = "https://json-schema.org/draft/2020-12/schema", Be = "https://kitn.ai/schemas/card/form.schema.json",
|
|
48
|
+
}, Je = "https://json-schema.org/draft/2020-12/schema", Be = "https://kitn.ai/schemas/card/form.schema.json", He = "kai-form data (a form definition)", Ye = "The CardEnvelope.data for a card of type 'form': a JSON Schema (type:'object') describing the fields kai-form renders, plus optional x-kai-* UI hints. The same schema validates the submission.", Ke = "object", We = ["type", "properties"], Ge = { type: { const: "object" }, title: { type: "string" }, description: { type: "string" }, required: { type: "array", items: { type: "string" } }, properties: { type: "object", description: "The fields to collect, keyed by property name. Each value is a JSON Schema field definition (type, title, description, enum, minimum/maximum, minLength/maxLength, pattern, items) plus optional x-kai-* UI hints. Only the display-format hints are spelled out below; the rest of a field definition is ordinary JSON Schema.", additionalProperties: { type: "object", properties: { "x-kai-format": { description: "Display format for a string field. `tel`, `ssn` and `credit-card` apply that type's standard mask and submit digits only; `custom` masks with `x-kai-mask` and submits the formatted value. Omit it for a plain text field.", enum: ["tel", "ssn", "credit-card", "custom"] }, "x-kai-mask": { description: 'The mask pattern, used only with `x-kai-format: "custom"`: `#` one digit, `@` one letter or digit, `*` one letter or digit that may be obscured; every other character is a literal shown as typed, e.g. `CHG-####`.', type: "string", maxLength: 64 }, "x-kai-mask-guide": { description: "Placeholder text shown at the unfilled positions of the mask, aligned character for character with the pattern (`mm/dd/yyyy` for `##/##/####`). Derived from the pattern when omitted.", type: "string", maxLength: 64 } } } }, "x-kai-order": { type: "array", items: { type: "string" } }, "x-kai-inlineMax": { type: "integer", minimum: 1 }, "x-kai-submitLabel": { type: "string" }, "x-kai-dismissible": { type: "boolean" }, "x-kai-actions": { type: "array", items: { type: "object", required: ["id", "label"], properties: { id: { type: "string" }, label: { type: "string" }, variant: { enum: ["default", "ghost", "outline"] } } } } }, Ve = {
|
|
49
49
|
$schema: Je,
|
|
50
50
|
$id: Be,
|
|
51
|
-
title:
|
|
52
|
-
description:
|
|
51
|
+
title: He,
|
|
52
|
+
description: Ye,
|
|
53
53
|
type: Ke,
|
|
54
54
|
required: We,
|
|
55
|
-
properties:
|
|
55
|
+
properties: Ge
|
|
56
56
|
}, Xe = "https://json-schema.org/draft/2020-12/schema", Qe = "https://kitn.ai/schemas/card/link.schema.json", Ze = "LinkPreviewData", et = "Rich link / Open-Graph preview payload. The card renders from this; it never fetches.", tt = "object", it = ["url"], nt = false, ot = { url: { type: "string", format: "uri", description: "Canonical destination. Opened via the contract `open` verb (target 'tab').", "x-kai-format": "url" }, title: { type: "string", description: "OG title (og:title). Falls back to the domain when absent.", maxLength: 300 }, description: { type: "string", description: "OG description (og:description). Clamped to 3 lines in the UI.", maxLength: 1e3 }, image: { type: "string", format: "uri", description: "Preview image (og:image). Optional; the card degrades gracefully when missing or it fails to load.", "x-kai-format": "url" }, imageAlt: { type: "string", description: "Alt text for the preview image. Defaults to the title (or empty = decorative) when omitted.", maxLength: 300 }, favicon: { type: "string", format: "uri", description: "Site favicon shown next to the domain.", "x-kai-format": "url" }, domain: { type: "string", description: "Display domain (e.g. 'example.com'). Derived from `url` when omitted.", maxLength: 253 }, siteName: { type: "string", description: "OG site name (og:site_name). Shown in place of the domain when present.", maxLength: 200 } }, rt = {
|
|
57
57
|
$schema: Xe,
|
|
58
58
|
$id: Qe,
|
|
@@ -62,14 +62,14 @@ const H = [{ required: ["src"] }, { required: ["files"] }], K = { src: { type: "
|
|
|
62
62
|
required: it,
|
|
63
63
|
additionalProperties: nt,
|
|
64
64
|
properties: ot
|
|
65
|
-
}, ht = "https://json-schema.org/draft/2020-12/schema", ft = "https://kitn.ai/schemas/card/tasks.schema.json", yt = "TasksCardData", gt = "Data payload for a `tasks` card (CardEnvelope.data when type='tasks').", bt = "object", vt = ["tasks"],
|
|
65
|
+
}, ht = "https://json-schema.org/draft/2020-12/schema", ft = "https://kitn.ai/schemas/card/tasks.schema.json", yt = "TasksCardData", gt = "Data payload for a `tasks` card (CardEnvelope.data when type='tasks').", bt = "object", vt = ["tasks"], wt = { mode: { type: "string", enum: ["select", "progress"], default: "select", description: "'select' (default) = toggle + a confirm button that emits `submit`. 'progress' = an onboarding-checklist look (header `done / total` count, circular indicators, per-item title/description, no confirm button, where checking a row IS the action). Both share the selection model.", "x-kai-mode": true }, heading: { type: "string", description: "Optional in-body heading; distinct from CardEnvelope.title." }, tasks: { type: "array", minItems: 1, description: "The selectable rows, rendered in order.", items: { type: "object", required: ["id", "label"], properties: { id: { type: "string", minLength: 1, description: "Stable id; the selected ids are returned in the result. Unique within `tasks`.", "x-kai-unique": true }, label: { type: "string", minLength: 1, description: "Row label." }, description: { type: "string", description: "Optional secondary line under the label." }, checked: { type: "boolean", default: false, description: "Initial checked state of the row." }, disabled: { type: "boolean", default: false, description: "Row is shown but not toggleable (and excluded from select-all)." } } } }, selectAll: { type: "boolean", default: false, description: "Render a master select-all checkbox above the list.", "x-kai-control": "select-all" }, confirmLabel: { type: "string", default: "Confirm", description: "Label for the confirm button." }, allowEmpty: { type: "boolean", default: false, description: "If true, confirm is enabled with zero selected (emits { selected: [] }). If false, confirm is disabled until >=1 selected." }, min: { type: "integer", minimum: 0, description: "Optional minimum number that must be selected to confirm.", "x-kai-select-min": true }, max: { type: "integer", minimum: 1, description: "Optional maximum selectable; further toggles are blocked once reached.", "x-kai-select-max": true }, dismissible: { type: "boolean", default: false, description: "Show a close affordance that emits the `dismiss` verb. The card collapses to a re-openable 'dismissed' stub; the host decides via `onReopen` whether it comes back live or `expired`." } }, $t = {
|
|
66
66
|
$schema: ht,
|
|
67
67
|
$id: ft,
|
|
68
68
|
title: yt,
|
|
69
69
|
description: gt,
|
|
70
70
|
type: bt,
|
|
71
71
|
required: vt,
|
|
72
|
-
properties:
|
|
72
|
+
properties: wt
|
|
73
73
|
}, S = "kai_";
|
|
74
74
|
function R(e) {
|
|
75
75
|
return `${S}${e}`;
|
|
@@ -267,8 +267,8 @@ function x(e, t, i, n, r) {
|
|
|
267
267
|
return;
|
|
268
268
|
}
|
|
269
269
|
for (const [p, c] of Object.entries(e)) {
|
|
270
|
-
const
|
|
271
|
-
if (!
|
|
270
|
+
const u = t.keywords[p];
|
|
271
|
+
if (!u) {
|
|
272
272
|
r.push({
|
|
273
273
|
path: i,
|
|
274
274
|
keyword: p,
|
|
@@ -276,12 +276,12 @@ function x(e, t, i, n, r) {
|
|
|
276
276
|
});
|
|
277
277
|
continue;
|
|
278
278
|
}
|
|
279
|
-
if (
|
|
280
|
-
r.push({ path: i, keyword: p, reason: `not supported (${
|
|
279
|
+
if (u.status === "unsupported") {
|
|
280
|
+
r.push({ path: i, keyword: p, reason: `not supported (${u.note})` });
|
|
281
281
|
continue;
|
|
282
282
|
}
|
|
283
|
-
if (
|
|
284
|
-
const h =
|
|
283
|
+
if (u.status === "supported" && u.value) {
|
|
284
|
+
const h = u.value(c);
|
|
285
285
|
h && r.push({ path: i, keyword: p, reason: h });
|
|
286
286
|
}
|
|
287
287
|
}
|
|
@@ -295,11 +295,11 @@ function x(e, t, i, n, r) {
|
|
|
295
295
|
keyword: "properties",
|
|
296
296
|
reason: `a free-form object (\`type: "object"\` with no \`properties\`) has no grammar under ${t.label}; with the mandatory \`additionalProperties: false\` it can only ever be \`{}\`, which is not what the schema means`
|
|
297
297
|
}), a && t.requireAllPropertiesRequired && o) {
|
|
298
|
-
const p = Array.isArray(e.required) ? e.required.map(String) : [], c = Object.keys(e.properties).filter((
|
|
298
|
+
const p = Array.isArray(e.required) ? e.required.map(String) : [], c = Object.keys(e.properties).filter((u) => !p.includes(u));
|
|
299
299
|
c.length > 0 && r.push({
|
|
300
300
|
path: i,
|
|
301
301
|
keyword: "required",
|
|
302
|
-
reason: `${t.label} requires every property in \`required\`; missing ${c.map((
|
|
302
|
+
reason: `${t.label} requires every property in \`required\`; missing ${c.map((u) => `\`${u}\``).join(", ")} (emulate optional with a \`null\` union)`
|
|
303
303
|
});
|
|
304
304
|
}
|
|
305
305
|
n && !t.allowRootAnyOf && "anyOf" in e && r.push({
|
|
@@ -314,25 +314,25 @@ function x(e, t, i, n, r) {
|
|
|
314
314
|
keyword: "type",
|
|
315
315
|
reason: `unconstrained subschema (annotations only); ${t.label} compiles the schema to a grammar and has no way to express "any JSON". Give it a \`type\``
|
|
316
316
|
});
|
|
317
|
-
const
|
|
317
|
+
const l = (p) => {
|
|
318
318
|
var c;
|
|
319
319
|
return ((c = t.keywords[p]) == null ? void 0 : c.status) === "unsupported";
|
|
320
320
|
};
|
|
321
321
|
for (const p of kt) {
|
|
322
322
|
const c = e[p];
|
|
323
|
-
if (!(!O(c) ||
|
|
324
|
-
for (const [
|
|
325
|
-
x(h, t, p === "properties" ? jt(i,
|
|
323
|
+
if (!(!O(c) || l(p)))
|
|
324
|
+
for (const [u, h] of Object.entries(c))
|
|
325
|
+
x(h, t, p === "properties" ? jt(i, u) : `${i}/${p}[${u}]`, false, r);
|
|
326
326
|
}
|
|
327
327
|
for (const p of xt) {
|
|
328
|
-
if (!(p in e) ||
|
|
328
|
+
if (!(p in e) || l(p)) continue;
|
|
329
329
|
const c = e[p];
|
|
330
330
|
typeof c != "boolean" && x(c, t, p === "items" ? `${i === "(root)" ? "(root)" : i}[]` : `${i}/${p}`, false, r);
|
|
331
331
|
}
|
|
332
332
|
O(e.additionalProperties) && x(e.additionalProperties, t, `${i}/additionalProperties`, false, r);
|
|
333
333
|
for (const p of Ot) {
|
|
334
334
|
const c = e[p];
|
|
335
|
-
!Array.isArray(c) ||
|
|
335
|
+
!Array.isArray(c) || l(p) || c.forEach((u, h) => x(u, t, `${i}/${p}[${h}]`, false, r));
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
const At = Object.freeze({
|
|
@@ -347,10 +347,10 @@ const At = Object.freeze({
|
|
|
347
347
|
class qt extends Error {
|
|
348
348
|
constructor(i, n) {
|
|
349
349
|
super(It(i, n));
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
350
|
+
$(this, "name", "UnsupportedCardToolSchemaError");
|
|
351
|
+
$(this, "provider");
|
|
352
|
+
$(this, "source");
|
|
353
|
+
$(this, "cards");
|
|
354
354
|
this.provider = i.id, this.source = i.source, this.cards = n;
|
|
355
355
|
}
|
|
356
356
|
/** Every distinct keyword named across every failing card, deduped, in order. */
|
|
@@ -393,19 +393,19 @@ function Rt(e, t) {
|
|
|
393
393
|
for (const o of t) {
|
|
394
394
|
const a = Ct(o, e[o]);
|
|
395
395
|
if (a.length === 0) continue;
|
|
396
|
-
for (const
|
|
397
|
-
if (f(
|
|
398
|
-
for (const [p, c] of Object.entries(
|
|
396
|
+
for (const l of a)
|
|
397
|
+
if (f(l.properties))
|
|
398
|
+
for (const [p, c] of Object.entries(l.properties))
|
|
399
399
|
p in i || (i[p] = c);
|
|
400
400
|
const s = a.map(
|
|
401
|
-
(
|
|
401
|
+
(l) => new Set(Array.isArray(l.required) ? l.required.map(String) : [])
|
|
402
402
|
);
|
|
403
403
|
if (o === "allOf")
|
|
404
|
-
for (const
|
|
404
|
+
for (const l of s) for (const p of l) n.add(p);
|
|
405
405
|
else {
|
|
406
|
-
const [
|
|
407
|
-
for (const c of
|
|
408
|
-
p.every((
|
|
406
|
+
const [l, ...p] = s;
|
|
407
|
+
for (const c of l)
|
|
408
|
+
p.every((u) => u.has(c)) && n.add(c);
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
Object.keys(i).length > 0 && (e.properties = i);
|
|
@@ -435,12 +435,12 @@ function _t(e, t, i) {
|
|
|
435
435
|
let r = e;
|
|
436
436
|
if (n.path !== "")
|
|
437
437
|
for (const a of n.path.split(".")) {
|
|
438
|
-
const s = f(r.properties) ? r.properties : void 0,
|
|
439
|
-
if (!f(
|
|
438
|
+
const s = f(r.properties) ? r.properties : void 0, l = s == null ? void 0 : s[a];
|
|
439
|
+
if (!f(l))
|
|
440
440
|
throw new TypeError(
|
|
441
441
|
`cardTools: require on '${t}': path '${n.path}' does not resolve in the schema`
|
|
442
442
|
);
|
|
443
|
-
r =
|
|
443
|
+
r = l;
|
|
444
444
|
}
|
|
445
445
|
const o = r.type === "array" || r.type === void 0 && "items" in r;
|
|
446
446
|
if (o && n.minItems !== void 0 && (r.minItems = n.minItems), !o && n.minItems !== void 0)
|
|
@@ -454,8 +454,8 @@ function _t(e, t, i) {
|
|
|
454
454
|
throw new TypeError(
|
|
455
455
|
`cardTools: require on '${t}' at path '${n.path}': '${p}' is not a property of that node — a required name with no property would make the tool unsatisfiable`
|
|
456
456
|
);
|
|
457
|
-
const
|
|
458
|
-
a.required = [.../* @__PURE__ */ new Set([...
|
|
457
|
+
const l = Array.isArray(a.required) ? a.required.map(String) : [];
|
|
458
|
+
a.required = [.../* @__PURE__ */ new Set([...l, ...n.required])];
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
461
|
}
|
|
@@ -472,7 +472,7 @@ function k(e, t) {
|
|
|
472
472
|
const a = i[o];
|
|
473
473
|
if (!f(a)) continue;
|
|
474
474
|
const s = {};
|
|
475
|
-
for (const [
|
|
475
|
+
for (const [l, p] of Object.entries(a)) s[l] = k(p, t);
|
|
476
476
|
i[o] = s;
|
|
477
477
|
}
|
|
478
478
|
for (const o of zt)
|
|
@@ -516,18 +516,18 @@ function Bt(e, t, i) {
|
|
|
516
516
|
return typeof o == "string" && o.length > 0 ? o : `Render a \`${e}\` card.`;
|
|
517
517
|
}
|
|
518
518
|
function v(e, t) {
|
|
519
|
-
var
|
|
519
|
+
var u;
|
|
520
520
|
const i = t === void 0, n = i ? e : t;
|
|
521
521
|
if (!f(n) || typeof n.provider != "string")
|
|
522
522
|
throw new TypeError('cardTools: an options object with a `provider` is required, e.g. cardTools({ provider: "openai" })');
|
|
523
523
|
const { schemas: r, descriptions: o } = Jt(
|
|
524
|
-
i ?
|
|
524
|
+
i ? Ht() : e
|
|
525
525
|
), a = n.strict === true;
|
|
526
526
|
if (n.provider === "jsonschema" && a)
|
|
527
527
|
throw new TypeError(
|
|
528
528
|
'cardTools: `strict: true` is provider-specific and cannot be checked for `provider: "jsonschema"`, because the two documented strict subsets differ (see @kitn.ai/ui/schemas provider-subsets). Pass provider "openai" or "anthropic" to project and check strictly, or drop `strict`.'
|
|
529
529
|
);
|
|
530
|
-
const s = a ? n.provider === "openai" ? P : _ : null,
|
|
530
|
+
const s = a ? n.provider === "openai" ? P : _ : null, l = {
|
|
531
531
|
closeObjects: s !== null,
|
|
532
532
|
requireEveryProperty: s !== null && s.requireAllPropertiesRequired
|
|
533
533
|
}, p = [], c = [];
|
|
@@ -538,8 +538,8 @@ function v(e, t) {
|
|
|
538
538
|
`cardTools: require names card type '${h}', which this call does not carry (carries: ${Object.keys(r).join(", ") || "none"})`
|
|
539
539
|
);
|
|
540
540
|
}
|
|
541
|
-
for (const [h,
|
|
542
|
-
const g = R(h), b = Bt(h,
|
|
541
|
+
for (const [h, w] of Object.entries(r)) {
|
|
542
|
+
const g = R(h), b = Bt(h, w, o), d = k(w, l), m = s === null && n.provider !== "jsonschema" ? Pt(d, h) : void 0, I = m ? `${b} ${m}` : b, L = (u = n.require) == null ? void 0 : u[h];
|
|
543
543
|
if (L !== void 0 && _t(d, h, L), s) {
|
|
544
544
|
const E = St(d, s);
|
|
545
545
|
if (E.length > 0) {
|
|
@@ -552,10 +552,10 @@ function v(e, t) {
|
|
|
552
552
|
if (s && c.length > 0) throw new qt(s, c);
|
|
553
553
|
return p;
|
|
554
554
|
}
|
|
555
|
-
function
|
|
555
|
+
function Ht() {
|
|
556
556
|
return j;
|
|
557
557
|
}
|
|
558
|
-
const
|
|
558
|
+
const ui = {
|
|
559
559
|
form: "kai-form",
|
|
560
560
|
confirm: "kai-confirm",
|
|
561
561
|
tasks: "kai-tasks",
|
|
@@ -568,16 +568,16 @@ const li = {
|
|
|
568
568
|
choice: y(be),
|
|
569
569
|
confirm: y(Se),
|
|
570
570
|
embed: y(Pe),
|
|
571
|
-
form: y(
|
|
571
|
+
form: y(Ve),
|
|
572
572
|
link: y(rt),
|
|
573
|
-
tasks: y(
|
|
573
|
+
tasks: y($t)
|
|
574
574
|
}), hi = Object.freeze(
|
|
575
575
|
Object.keys(j)
|
|
576
576
|
);
|
|
577
577
|
function ii(e) {
|
|
578
578
|
return Object.prototype.hasOwnProperty.call(j, e);
|
|
579
579
|
}
|
|
580
|
-
const tags = { "kai-conversations": "conversation-list", "kai-conversation-item": "conversation-item", "kai-prompt-input": "prompt-input", "kai-chat": "chat", "kai-workspace": "chat-workspace", "kai-thread": "thread", "kai-thinking-bar": "thinking-bar", "kai-model-switcher": "model-switcher", "kai-attachments": "attachments", "kai-message": "message", "kai-markdown": "markdown", "kai-code-block": "code-block", "kai-reasoning": "reasoning", "kai-tool": "tool", "kai-context": "context-meter", "kai-feedback-bar": "feedback-bar", "kai-scope-picker": "chat-scope-picker", "kai-suggestions": "prompt-suggestions", "kai-file-upload": "file-upload", "kai-voice-input": "voice-input", "kai-audio-visualizer": "audio-visualizer", "kai-loader": "loader", "kai-text-shimmer": "text-shimmer", "kai-image": "image", "kai-checkpoint": "checkpoint", "kai-skills": "message-skills", "kai-source": "source", "kai-sources": "source", "kai-response-stream": "response-stream", "kai-empty": "empty", "kai-status": "status", "kai-nav": "nav", "kai-progress-bar": "progress-bar", "kai-coachmark": "coachmark", "kai-tabs": "tabs", "kai-voice-output": "voice-output", "kai-screen": "screen", "kai-chain-of-thought": "chain-of-thought", "kai-resizable": "resizable", "kai-resizable-item": "resizable", "kai-file-tree": "file-tree", "kai-artifact": "artifact", "kai-scroll-button": "scroll-button", "kai-popover": "popover", "kai-switch": "switch", "kai-button": "button", "kai-avatar": "avatar", "kai-badge": "badge", "kai-tooltip": "tooltip", "kai-notice": "notice", "kai-icon": "icon", "kai-separator": "separator", "kai-scroll-area": "scroll-area", "kai-hover-card": "hover-card", "kai-skeleton": "skeleton", "kai-toast-region": "toast", "kai-card": "card", "kai-form": "form", "kai-link-preview": "link-preview", "kai-embed": "embed", "kai-confirm": "confirm-card", "kai-tasks": "tasks", "kai-choice": "choice", "kai-cards": "cards", "kai-compare": "compare", "kai-composer": "composer", "kai-menu": "menu", "kai-dropdown": "dropdown", "kai-command": "command", "kai-prompt-dock": "prompt-dock", "kai-segmented": "segmented", "kai-settings-group": "settings-group", "kai-setting-item": "setting-item", "kai-pane": "pane", "kai-pane-group": "pane-group", "kai-agent-card": "agent-card", "kai-dialog": "dialog", "kai-dock": "dock", "kai-input": "input", "kai-search": "search", "kai-kbd": "kbd", "kai-editable-label": "editable-label" };
|
|
580
|
+
const tags = { "kai-conversations": "conversation-list", "kai-conversation-item": "conversation-item", "kai-prompt-input": "prompt-input", "kai-chat": "chat", "kai-workspace": "chat-workspace", "kai-thread": "thread", "kai-thinking-bar": "thinking-bar", "kai-model-switcher": "model-switcher", "kai-attachments": "attachments", "kai-message": "message", "kai-markdown": "markdown", "kai-code-block": "code-block", "kai-reasoning": "reasoning", "kai-tool": "tool", "kai-context": "context-meter", "kai-feedback-bar": "feedback-bar", "kai-scope-picker": "chat-scope-picker", "kai-suggestions": "prompt-suggestions", "kai-file-upload": "file-upload", "kai-voice-input": "voice-input", "kai-audio-visualizer": "audio-visualizer", "kai-loader": "loader", "kai-text-shimmer": "text-shimmer", "kai-image": "image", "kai-checkpoint": "checkpoint", "kai-skills": "message-skills", "kai-source": "source", "kai-sources": "source", "kai-response-stream": "response-stream", "kai-empty": "empty", "kai-status": "status", "kai-nav": "nav", "kai-progress-bar": "progress-bar", "kai-coachmark": "coachmark", "kai-tabs": "tabs", "kai-voice-output": "voice-output", "kai-screen": "screen", "kai-chain-of-thought": "chain-of-thought", "kai-resizable": "resizable", "kai-resizable-item": "resizable", "kai-file-tree": "file-tree", "kai-artifact": "artifact", "kai-scroll-button": "scroll-button", "kai-popover": "popover", "kai-switch": "switch", "kai-checkbox": "checkbox", "kai-checkbox-group": "checkbox-group", "kai-radio-group": "radio-group", "kai-slider": "slider", "kai-select": "select", "kai-button": "button", "kai-avatar": "avatar", "kai-badge": "badge", "kai-tooltip": "tooltip", "kai-notice": "notice", "kai-icon": "icon", "kai-separator": "separator", "kai-scroll-area": "scroll-area", "kai-hover-card": "hover-card", "kai-skeleton": "skeleton", "kai-toast-region": "toast", "kai-card": "card", "kai-form": "form", "kai-link-preview": "link-preview", "kai-embed": "embed", "kai-confirm": "confirm-card", "kai-tasks": "tasks", "kai-choice": "choice", "kai-cards": "cards", "kai-compare": "compare", "kai-composer": "composer", "kai-menu": "menu", "kai-dropdown": "dropdown", "kai-command": "command", "kai-prompt-dock": "prompt-dock", "kai-segmented": "segmented", "kai-settings-group": "settings-group", "kai-setting-item": "setting-item", "kai-pane": "pane", "kai-pane-group": "pane-group", "kai-pane-grid": "pane-grid", "kai-agent-card": "agent-card", "kai-dialog": "dialog", "kai-dock": "dock", "kai-input": "input", "kai-search": "search", "kai-kbd": "kbd", "kai-editable-label": "editable-label" };
|
|
581
581
|
const PACKAGE_NAME$1 = "@kitn.ai/ui";
|
|
582
582
|
const MANIFEST_FILE = "custom-elements.json";
|
|
583
583
|
const SOURCE_TO_PACKAGE_ROOT = ["..", "..", ".."];
|
|
@@ -669,7 +669,7 @@ let _cardHosts;
|
|
|
669
669
|
function cardTagForType(type) {
|
|
670
670
|
if (!_cardTags) {
|
|
671
671
|
const present = new Set(listElements());
|
|
672
|
-
_cardTags = new Map(Object.entries(
|
|
672
|
+
_cardTags = new Map(Object.entries(ui).filter(([, tag]) => present.has(tag)));
|
|
673
673
|
}
|
|
674
674
|
return _cardTags.get(type);
|
|
675
675
|
}
|
|
@@ -860,7 +860,7 @@ const invariants = [
|
|
|
860
860
|
{
|
|
861
861
|
wrong: "window.open(card.url, '_blank');",
|
|
862
862
|
right: "const isNavigable = (u) => { try { return ['http:', 'https:', 'mailto:'].includes(new URL(u, location.href).protocol); } catch { return false; } };\nif (isNavigable(card.url)) window.open(card.url, '_blank', 'noopener,noreferrer');",
|
|
863
|
-
note: "THE try/catch IS NOT OPTIONAL: new URL() THROWS on an unparseable input like 'http://[', and an uncaught throw here crashes the render instead of blocking the link — worse than the bug you are fixing. Returning false is the whole contract. Resolving against location.href is deliberate: a relative or fragment href inherits http: and is allowed, which is what makes ordinary links keep working. REPO-INTERNAL ONLY: contributors inside this package use isSafeUrl/SAFE_SCHEMES from src/primitives/card-routing.ts, which has this exact shape — neither is reachable from the published package, so never emit an import for them."
|
|
863
|
+
note: "THE try/catch IS NOT OPTIONAL: new URL() THROWS on an unparseable input like 'http://[', and an uncaught throw here crashes the render instead of blocking the link — worse than the bug you are fixing. Returning false is the whole contract. Resolving against location.href is deliberate: a relative or fragment href inherits http: and is allowed, which is what makes ordinary links keep working. REPO-INTERNAL ONLY: contributors inside this package use isSafeUrl/SAFE_SCHEMES from src/primitives/url-scheme-policy.ts (re-exported from src/primitives/card-routing.ts for the existing call sites), which has this exact shape — neither is reachable from the published package, so never emit an import for them."
|
|
864
864
|
},
|
|
865
865
|
{
|
|
866
866
|
wrong: "<a href={source.url}>{source.title}</a>",
|
|
@@ -1064,8 +1064,755 @@ const surfaceRecipes = [
|
|
|
1064
1064
|
"untrusted-model-output"
|
|
1065
1065
|
],
|
|
1066
1066
|
corpus: ["packages/ui/README.md"]
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
// The hand-composed thread (rung-6 F-49): kai-chat deliberately absent —
|
|
1070
|
+
// kai-thread renders the transcript, kai-composer takes input, and the
|
|
1071
|
+
// HOST is every line of wiring between them. This record is the catalog
|
|
1072
|
+
// half; the full compiling host module is the `composed-thread` CODE
|
|
1073
|
+
// recipe (../recipes/composed-thread.ts, served by component_reference
|
|
1074
|
+
// { name: "composed-thread" } and compiled by verify:scaffold), which is
|
|
1075
|
+
// also this record's corpus. Ingredients with no wiring edge below are
|
|
1076
|
+
// still real members of the composition: kai-feedback-bar's events
|
|
1077
|
+
// terminate in the host rather than setting another element's property.
|
|
1078
|
+
// (kai-conversation-item used to be in that list as "presentational
|
|
1079
|
+
// standalone"; since F-45 tier 2 a standalone row activates itself, so
|
|
1080
|
+
// the rail is a real kai-select edge below.)
|
|
1081
|
+
id: "composed-thread",
|
|
1082
|
+
intent: "A full chat surface composed by hand from standalone elements — no <kai-chat>. The host module owns the store, streams through createAssistantStream + the wire reader, stages attachments as data: URIs, and drives the toast region as data.",
|
|
1083
|
+
archetypes: ["full-screen"],
|
|
1084
|
+
targets: ["bundler"],
|
|
1085
|
+
ingredients: [
|
|
1086
|
+
"kai-thread",
|
|
1087
|
+
"kai-composer",
|
|
1088
|
+
"kai-attachments",
|
|
1089
|
+
"kai-toast-region",
|
|
1090
|
+
"kai-conversation-item",
|
|
1091
|
+
"kai-feedback-bar"
|
|
1092
|
+
],
|
|
1093
|
+
backend: { endpoint: "consumer-owned", reader: "readOpenAIStream" },
|
|
1094
|
+
wiring: [
|
|
1095
|
+
{
|
|
1096
|
+
from: "kai-composer",
|
|
1097
|
+
event: "kai-submit",
|
|
1098
|
+
to: "kai-thread",
|
|
1099
|
+
property: "messages",
|
|
1100
|
+
note: "detail is {doc, text, entities} — read detail.text (NOT detail.value, which is kai-chat/kai-prompt-input's shape). The host appends the user turn plus any staged file parts and assigns a NEW array; the reply then streams onto the same property via createAssistantStream"
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
from: "kai-attachments",
|
|
1104
|
+
event: "kai-remove",
|
|
1105
|
+
to: "kai-attachments",
|
|
1106
|
+
property: "items",
|
|
1107
|
+
note: "detail is {id}; the tray never mutates its own list — the host filters and reassigns items, and a data: URI needs no revocation on removal (attachment-blob-url is the rule this recipe corrects: F-44)"
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
from: "kai-toast-region",
|
|
1111
|
+
event: "kai-dismiss",
|
|
1112
|
+
to: "kai-toast-region",
|
|
1113
|
+
property: "toasts",
|
|
1114
|
+
note: "detail is {id}; the region is driven as DATA because this app places its own and owns the array — the imperative toast() would ADOPT this markup-placed region (it mounts its own only when none exists) and bind its store over the toasts property, so pick one API per region"
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
from: "kai-conversation-item",
|
|
1118
|
+
event: "kai-select",
|
|
1119
|
+
to: "kai-thread",
|
|
1120
|
+
property: "messages",
|
|
1121
|
+
note: "detail is {id}; STANDALONE rows only (F-45 tier 2) — outside <kai-conversations> the row body is a tabbable button and click / Enter / Space fire kai-select on the item (non-bubbling: listen on the row). Inside a container this never fires; the container's kai-conversation-select is the one activation event. The container's list story (roving tabindex, arrow keys) stays the host's job in a hand-rolled rail"
|
|
1122
|
+
}
|
|
1123
|
+
],
|
|
1124
|
+
invariants: [
|
|
1125
|
+
"reactivity-two-halves",
|
|
1126
|
+
"props-not-attributes",
|
|
1127
|
+
"events-non-bubbling",
|
|
1128
|
+
"host-coordinates",
|
|
1129
|
+
"kit-parses-consumer-fetches",
|
|
1130
|
+
"untrusted-model-output"
|
|
1131
|
+
],
|
|
1132
|
+
corpus: [
|
|
1133
|
+
"packages/ui/src/agent-tooling/recipes/composed-thread.ts",
|
|
1134
|
+
"packages/ui/src/agent-tooling/catalog/surfaces.test.ts"
|
|
1135
|
+
]
|
|
1067
1136
|
}
|
|
1068
1137
|
];
|
|
1138
|
+
const composedThread = {
|
|
1139
|
+
id: "composed-thread",
|
|
1140
|
+
title: "Hand-composed thread (no kai-chat)",
|
|
1141
|
+
intent: "A full chat surface composed from standalone elements — kai-thread renders the transcript, kai-composer takes input, kai-attachments stages files, kai-toast-region notifies, kai-conversation-item rows form the rail — with a host module doing every bit of wiring (host-coordinates: the kit has no store). Streams through createAssistantStream + createMockResponder, so it runs with no backend and no key; swapping the mock for a real /api/chat changes one line.",
|
|
1142
|
+
ingredients: [
|
|
1143
|
+
"kai-thread",
|
|
1144
|
+
"kai-composer",
|
|
1145
|
+
"kai-attachments",
|
|
1146
|
+
"kai-toast-region",
|
|
1147
|
+
"kai-conversation-item",
|
|
1148
|
+
"kai-feedback-bar"
|
|
1149
|
+
],
|
|
1150
|
+
notes: [
|
|
1151
|
+
"`<kai-composer>` is the BARE editing surface — no send button, no attachments. The paperclip, the hidden file input, the staging tray and the send button here are the host's. For the batteries-included composer row, use `<kai-prompt-input>` instead.",
|
|
1152
|
+
"Attachments are staged as `data:` URIs, NEVER as object URLs (`createObjectURL`). A `blob:` URL resolves only inside the tab that minted it; `toOpenAIMessages` / `toAnthropicMessages` refuse it with a written reason, and before they refused it an attachment-only turn reached the model as nothing at all.",
|
|
1153
|
+
"The HOST resolves tool calls. The mock (like a real provider) only ANNOUNCES one — walk `result.toolCalls` after the read and call `stream.upsertTool(id, { state: 'output-available', output })`, or the panel sits at input-available forever.",
|
|
1154
|
+
"The toast region is driven as DATA (`toasts` property, `kai-dismiss` event) because this app places its own `<kai-toast-region>` and owns the array. Note: `toast()` ADOPTS a markup-placed region (it creates its own only when none exists) and binds its store over `toasts`, replacing a data-driven array — so per region, own the array or call `toast()`, not both.",
|
|
1155
|
+
"A standalone `<kai-conversation-item>` (outside `<kai-conversations>`) activates itself: its body is a tabbable button and click / Enter / Space fire `kai-select` on the item with `{ id }` (non-bubbling — listen on the item). What it does NOT carry is the container's list story — roving tabindex and arrow-key traversal stay yours in a hand-rolled rail.",
|
|
1156
|
+
"`<kai-thread>` fills the height its parent gives it and scrolls internally, which is why `.pane` is a flex column and `.thread` is `flex: 1; min-height: 0`."
|
|
1157
|
+
],
|
|
1158
|
+
files: [
|
|
1159
|
+
{
|
|
1160
|
+
path: "index.html",
|
|
1161
|
+
lang: "html",
|
|
1162
|
+
code: `<!doctype html>
|
|
1163
|
+
<html lang="en">
|
|
1164
|
+
<head>
|
|
1165
|
+
<meta charset="UTF-8" />
|
|
1166
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1167
|
+
<title>Composed thread</title>
|
|
1168
|
+
<link rel="stylesheet" href="/src/styles.css" />
|
|
1169
|
+
</head>
|
|
1170
|
+
<body>
|
|
1171
|
+
<!-- The thread is composed by hand from standalone kai-* elements;
|
|
1172
|
+
the kai-chat drop-in is deliberately absent from this app. -->
|
|
1173
|
+
<div class="app">
|
|
1174
|
+
<aside class="rail">
|
|
1175
|
+
<div class="rail-head">
|
|
1176
|
+
<span class="rail-title">Conversations</span>
|
|
1177
|
+
<button id="new-chat" class="ghost-btn" type="button">New</button>
|
|
1178
|
+
</div>
|
|
1179
|
+
<!-- kai-conversation-item rows are created in src/main.ts -->
|
|
1180
|
+
<nav id="conversations" class="rail-list" aria-label="Conversations"></nav>
|
|
1181
|
+
</aside>
|
|
1182
|
+
|
|
1183
|
+
<main class="pane">
|
|
1184
|
+
<header class="pane-head">
|
|
1185
|
+
<h1 id="pane-title">New chat</h1>
|
|
1186
|
+
<span class="badge">mock responder — no provider contacted</span>
|
|
1187
|
+
</header>
|
|
1188
|
+
|
|
1189
|
+
<kai-thread id="thread" class="thread" prose-size="sm">
|
|
1190
|
+
<div slot="empty" class="empty">
|
|
1191
|
+
<strong>Compose a message</strong>
|
|
1192
|
+
<p>Attach a file with the paperclip and send it — it renders on your message.</p>
|
|
1193
|
+
</div>
|
|
1194
|
+
</kai-thread>
|
|
1195
|
+
|
|
1196
|
+
<!-- Shown once an assistant turn settles. -->
|
|
1197
|
+
<kai-feedback-bar
|
|
1198
|
+
id="feedback"
|
|
1199
|
+
class="feedback"
|
|
1200
|
+
hidden
|
|
1201
|
+
bar-title="Was this reply helpful?"
|
|
1202
|
+
collect-detail
|
|
1203
|
+
detail-title="What went wrong?"
|
|
1204
|
+
detail-placeholder="Optional details…"
|
|
1205
|
+
submit-label="Send feedback"
|
|
1206
|
+
thanks-message="Thanks — logged locally."
|
|
1207
|
+
></kai-feedback-bar>
|
|
1208
|
+
|
|
1209
|
+
<div class="composer-bar">
|
|
1210
|
+
<!-- Staged (not yet sent) attachments. kai-composer has no attachment
|
|
1211
|
+
support of its own, so the tray + the picker below are ours. -->
|
|
1212
|
+
<kai-attachments
|
|
1213
|
+
id="staged"
|
|
1214
|
+
class="staged"
|
|
1215
|
+
variant="inline"
|
|
1216
|
+
removable
|
|
1217
|
+
hover-card
|
|
1218
|
+
show-media-type
|
|
1219
|
+
hidden
|
|
1220
|
+
></kai-attachments>
|
|
1221
|
+
|
|
1222
|
+
<div class="composer-row">
|
|
1223
|
+
<button id="attach" class="icon-btn" type="button" title="Attach a file" aria-label="Attach a file">
|
|
1224
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>
|
|
1225
|
+
</button>
|
|
1226
|
+
<input id="file-input" type="file" multiple hidden />
|
|
1227
|
+
<kai-composer
|
|
1228
|
+
id="composer"
|
|
1229
|
+
class="composer"
|
|
1230
|
+
placeholder="Ask anything… (Enter to send)"
|
|
1231
|
+
></kai-composer>
|
|
1232
|
+
<button id="send" class="send-btn" type="button">Send</button>
|
|
1233
|
+
</div>
|
|
1234
|
+
</div>
|
|
1235
|
+
</main>
|
|
1236
|
+
</div>
|
|
1237
|
+
|
|
1238
|
+
<!-- Placed explicitly rather than letting toast() auto-mount one. -->
|
|
1239
|
+
<kai-toast-region id="toasts" position="bottom-right" appearance="card"></kai-toast-region>
|
|
1240
|
+
|
|
1241
|
+
<script type="module" src="/src/main.ts"><\/script>
|
|
1242
|
+
</body>
|
|
1243
|
+
</html>
|
|
1244
|
+
`
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
path: "src/main.ts",
|
|
1248
|
+
lang: "ts",
|
|
1249
|
+
code: `// The whole app: a chat surface composed by hand from standalone kai-*
|
|
1250
|
+
// elements. <kai-chat> is deliberately not used — kai-thread renders the
|
|
1251
|
+
// transcript, kai-composer takes input, and this module is the host that
|
|
1252
|
+
// wires one to the other (the kit has no store: host-coordinates).
|
|
1253
|
+
import '@kitn.ai/ui/elements'; // registers every <kai-*>; must come first
|
|
1254
|
+
import '@kitn.ai/ui/theme.tokens.css';
|
|
1255
|
+
|
|
1256
|
+
import type {
|
|
1257
|
+
AttachmentData,
|
|
1258
|
+
ChatMessage,
|
|
1259
|
+
KaiAttachmentsElement,
|
|
1260
|
+
KaiComposerElement,
|
|
1261
|
+
KaiConversationItemElement,
|
|
1262
|
+
KaiFeedbackBarElement,
|
|
1263
|
+
KaiThreadElement,
|
|
1264
|
+
KaiToastRegionElement,
|
|
1265
|
+
MessagePart,
|
|
1266
|
+
ToastItem,
|
|
1267
|
+
} from '@kitn.ai/ui/elements';
|
|
1268
|
+
import { createAssistantStream, createMockResponder } from '@kitn.ai/ui/state';
|
|
1269
|
+
import { readOpenAIStream } from '@kitn.ai/ui/wire';
|
|
1270
|
+
|
|
1271
|
+
// ── the fake model ──────────────────────────────────────────────────────────
|
|
1272
|
+
// No key, no network, no provider. createMockResponder yields real OpenAI
|
|
1273
|
+
// chat-completions SSE frames that readOpenAIStream parses on exactly the path
|
|
1274
|
+
// a live route would take. Replies cycle one per turn; the third scripts a
|
|
1275
|
+
// tool call so the thread's tool panel gets exercised too.
|
|
1276
|
+
const mockResponse = createMockResponder({
|
|
1277
|
+
replies: [
|
|
1278
|
+
'Hey — this thread is composed from standalone \`kai-*\` elements: ' +
|
|
1279
|
+
'\`kai-thread\` for the transcript, \`kai-composer\` for input, and a host ' +
|
|
1280
|
+
'module wiring them together.\\n\\nAttach a file with the paperclip and send it.',
|
|
1281
|
+
'Got it. Anything you attach is staged in \`kai-attachments\` next to the ' +
|
|
1282
|
+
'composer, then rides along as a \`file\` part on the message you send.',
|
|
1283
|
+
{
|
|
1284
|
+
text: 'Let me look that up.',
|
|
1285
|
+
toolCalls: [
|
|
1286
|
+
{ name: 'search_docs', arguments: { query: 'composing a kai thread', limit: 3 } },
|
|
1287
|
+
],
|
|
1288
|
+
},
|
|
1289
|
+
'Streaming, tool calls, attachments and toasts all run through the same ' +
|
|
1290
|
+
'host wiring — swapping the mock for a real \`/api/chat\` changes one line.',
|
|
1291
|
+
],
|
|
1292
|
+
chunkSize: 2,
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
// ── conversations ───────────────────────────────────────────────────────────
|
|
1296
|
+
|
|
1297
|
+
interface Conversation {
|
|
1298
|
+
id: string;
|
|
1299
|
+
title: string;
|
|
1300
|
+
messages: ChatMessage[];
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const conversations: Conversation[] = [
|
|
1304
|
+
{ id: crypto.randomUUID(), title: 'New chat', messages: [] },
|
|
1305
|
+
];
|
|
1306
|
+
let activeId = conversations[0].id;
|
|
1307
|
+
|
|
1308
|
+
const active = (): Conversation =>
|
|
1309
|
+
conversations.find((c) => c.id === activeId) ?? conversations[0];
|
|
1310
|
+
|
|
1311
|
+
// ── element handles (typed once, after upgrade) ─────────────────────────────
|
|
1312
|
+
|
|
1313
|
+
const el = <T extends HTMLElement>(id: string): T =>
|
|
1314
|
+
document.getElementById(id) as T;
|
|
1315
|
+
|
|
1316
|
+
/** A picked File as a \`data:\` URI — the ONE form every consumer of
|
|
1317
|
+
* \`AttachmentData.url\` accepts.
|
|
1318
|
+
*
|
|
1319
|
+
* NOT an object URL: a \`blob:\` URL resolves only inside the tab that minted
|
|
1320
|
+
* it, so it renders a perfect preview here and is meaningless to anything
|
|
1321
|
+
* downstream — \`toOpenAIMessages\` / \`toAnthropicMessages\` refuse it with a
|
|
1322
|
+
* written reason, and before they refused it an attachment-only turn reached
|
|
1323
|
+
* the model as nothing at all. A data URI previews identically, survives a
|
|
1324
|
+
* reload, needs no revocation bookkeeping, and is what both providers take. */
|
|
1325
|
+
const readAsDataUrl = (file: File): Promise<string> =>
|
|
1326
|
+
new Promise((resolve, reject) => {
|
|
1327
|
+
const reader = new FileReader();
|
|
1328
|
+
reader.onload = () => resolve(String(reader.result));
|
|
1329
|
+
reader.onerror = () => reject(reader.error ?? new Error('Could not read ' + file.name));
|
|
1330
|
+
reader.readAsDataURL(file);
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
let staged: AttachmentData[] = [];
|
|
1334
|
+
let sending = false;
|
|
1335
|
+
|
|
1336
|
+
async function main() {
|
|
1337
|
+
// kai-* register through an async dynamic import (SSR-safety), so a property
|
|
1338
|
+
// set before the upgrade is dropped. Wait for every element this host drives.
|
|
1339
|
+
await Promise.all(
|
|
1340
|
+
[
|
|
1341
|
+
'kai-thread',
|
|
1342
|
+
'kai-composer',
|
|
1343
|
+
'kai-attachments',
|
|
1344
|
+
'kai-conversation-item',
|
|
1345
|
+
'kai-feedback-bar',
|
|
1346
|
+
'kai-toast-region',
|
|
1347
|
+
].map((tag) => customElements.whenDefined(tag)),
|
|
1348
|
+
);
|
|
1349
|
+
|
|
1350
|
+
const thread = el<KaiThreadElement>('thread');
|
|
1351
|
+
const composer = el<KaiComposerElement>('composer');
|
|
1352
|
+
const stagedEl = el<KaiAttachmentsElement>('staged');
|
|
1353
|
+
const feedback = el<KaiFeedbackBarElement>('feedback');
|
|
1354
|
+
const toastRegion = el<KaiToastRegionElement>('toasts');
|
|
1355
|
+
const rail = el<HTMLElement>('conversations');
|
|
1356
|
+
const title = el<HTMLElement>('pane-title');
|
|
1357
|
+
const sendBtn = el<HTMLButtonElement>('send');
|
|
1358
|
+
const attachBtn = el<HTMLButtonElement>('attach');
|
|
1359
|
+
const fileInput = el<HTMLInputElement>('file-input');
|
|
1360
|
+
|
|
1361
|
+
// ── toasts ────────────────────────────────────────────────────────────────
|
|
1362
|
+
// Driven as data rather than through the imperative toast(), because this app
|
|
1363
|
+
// places its own <kai-toast-region> and owns the list. Don't ALSO call
|
|
1364
|
+
// toast(): it would adopt this region and bind its own store over the
|
|
1365
|
+
// toasts property, replacing the array this module owns.
|
|
1366
|
+
let toasts: ToastItem[] = [];
|
|
1367
|
+
const setToasts = (next: ToastItem[]) => {
|
|
1368
|
+
toasts = next;
|
|
1369
|
+
toastRegion.toasts = toasts; // new array reference every time
|
|
1370
|
+
};
|
|
1371
|
+
const notify = (message: string, item: Partial<ToastItem> = {}) =>
|
|
1372
|
+
setToasts([...toasts, { id: crypto.randomUUID(), message, duration: 4000, ...item }]);
|
|
1373
|
+
|
|
1374
|
+
toastRegion.addEventListener('kai-dismiss', (event) => {
|
|
1375
|
+
const { id } = (event as CustomEvent<{ id: string }>).detail;
|
|
1376
|
+
setToasts(toasts.filter((t) => t.id !== id));
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// ── rendering ─────────────────────────────────────────────────────────────
|
|
1380
|
+
|
|
1381
|
+
// Reassign, never mutate: the array reference is what tells the element
|
|
1382
|
+
// something changed, and the row keys are the message objects themselves.
|
|
1383
|
+
const setMessages = (next: ChatMessage[]) => {
|
|
1384
|
+
active().messages = next;
|
|
1385
|
+
thread.messages = next;
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
function renderRail() {
|
|
1389
|
+
rail.replaceChildren(
|
|
1390
|
+
...conversations.map((conversation) => {
|
|
1391
|
+
const row = document.createElement(
|
|
1392
|
+
'kai-conversation-item',
|
|
1393
|
+
) as KaiConversationItemElement;
|
|
1394
|
+
row.conversationId = conversation.id;
|
|
1395
|
+
row.active = conversation.id === activeId;
|
|
1396
|
+
row.compact = true;
|
|
1397
|
+
row.textContent = conversation.title;
|
|
1398
|
+
|
|
1399
|
+
const meta = document.createElement('span');
|
|
1400
|
+
meta.slot = 'meta';
|
|
1401
|
+
meta.textContent = conversation.messages.length
|
|
1402
|
+
? conversation.messages.length + ' messages'
|
|
1403
|
+
: 'Empty';
|
|
1404
|
+
row.append(meta);
|
|
1405
|
+
|
|
1406
|
+
// Standalone rows activate THEMSELVES: outside <kai-conversations>
|
|
1407
|
+
// the item's body is a tabbable button and click / Enter / Space fire
|
|
1408
|
+
// kai-select on the item (non-bubbling — listen on the row, and take
|
|
1409
|
+
// the id from detail rather than re-deriving it). What standalone rows
|
|
1410
|
+
// do not get is the container's LIST story: roving tabindex and
|
|
1411
|
+
// arrow-key traversal stay yours in a hand-rolled rail.
|
|
1412
|
+
row.addEventListener('kai-select', (e) => {
|
|
1413
|
+
select((e as CustomEvent<{ id: string }>).detail.id);
|
|
1414
|
+
});
|
|
1415
|
+
return row;
|
|
1416
|
+
}),
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function select(id: string) {
|
|
1421
|
+
activeId = id;
|
|
1422
|
+
const conversation = active();
|
|
1423
|
+
title.textContent = conversation.title;
|
|
1424
|
+
thread.messages = conversation.messages;
|
|
1425
|
+
feedback.hidden = true;
|
|
1426
|
+
renderRail();
|
|
1427
|
+
composer.focus();
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function renderStaged() {
|
|
1431
|
+
stagedEl.items = staged; // JS property, new array each time
|
|
1432
|
+
stagedEl.hidden = staged.length === 0;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// ── attaching a file ──────────────────────────────────────────────────────
|
|
1436
|
+
// kai-composer has no attachment affordance of its own (unlike
|
|
1437
|
+
// kai-prompt-input), so the picker and the staging tray are the host's.
|
|
1438
|
+
|
|
1439
|
+
attachBtn.addEventListener('click', () => fileInput.click());
|
|
1440
|
+
|
|
1441
|
+
fileInput.addEventListener('change', () => {
|
|
1442
|
+
const picked = [...(fileInput.files ?? [])];
|
|
1443
|
+
fileInput.value = ''; // so the same file can be picked twice
|
|
1444
|
+
if (!picked.length) return;
|
|
1445
|
+
void (async () => {
|
|
1446
|
+
const added = await Promise.all(
|
|
1447
|
+
picked.map(
|
|
1448
|
+
async (file): Promise<AttachmentData> => ({
|
|
1449
|
+
id: crypto.randomUUID(),
|
|
1450
|
+
type: 'file',
|
|
1451
|
+
filename: file.name,
|
|
1452
|
+
mediaType: file.type || 'application/octet-stream',
|
|
1453
|
+
url: await readAsDataUrl(file), // data: URI — see readAsDataUrl
|
|
1454
|
+
}),
|
|
1455
|
+
),
|
|
1456
|
+
);
|
|
1457
|
+
staged = [...staged, ...added];
|
|
1458
|
+
renderStaged();
|
|
1459
|
+
notify(
|
|
1460
|
+
picked.length === 1 ? 'Attached ' + picked[0].name : 'Attached ' + picked.length + ' files',
|
|
1461
|
+
{ variant: 'success' },
|
|
1462
|
+
);
|
|
1463
|
+
})();
|
|
1464
|
+
composer.focus();
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
stagedEl.addEventListener('kai-remove', (event) => {
|
|
1468
|
+
const { id } = (event as CustomEvent<{ id: string }>).detail;
|
|
1469
|
+
// A data: URI needs no revocation — dropping the item is the whole cleanup.
|
|
1470
|
+
staged = staged.filter((item) => item.id !== id);
|
|
1471
|
+
renderStaged();
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// ── sending ───────────────────────────────────────────────────────────────
|
|
1475
|
+
|
|
1476
|
+
sendBtn.addEventListener('click', () => composer.send());
|
|
1477
|
+
|
|
1478
|
+
composer.addEventListener('kai-submit', (event) => {
|
|
1479
|
+
const { text } = (event as CustomEvent<{ text: string }>).detail;
|
|
1480
|
+
void send(text);
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
async function send(rawText: string) {
|
|
1484
|
+
const text = rawText.trim();
|
|
1485
|
+
// An attachment on its own is a legitimate message; empty + empty is not.
|
|
1486
|
+
if (sending || (!text && !staged.length)) return;
|
|
1487
|
+
|
|
1488
|
+
const attachments = staged;
|
|
1489
|
+
staged = [];
|
|
1490
|
+
renderStaged();
|
|
1491
|
+
composer.clear();
|
|
1492
|
+
|
|
1493
|
+
// A message's content is an ordered parts array — the file parts sit after
|
|
1494
|
+
// the text, which is how kai-message renders them under the bubble.
|
|
1495
|
+
const parts: MessagePart[] = [
|
|
1496
|
+
...(text ? [{ type: 'text', text } as const] : []),
|
|
1497
|
+
...attachments.map((attachment) => ({ type: 'file', attachment }) as const),
|
|
1498
|
+
];
|
|
1499
|
+
|
|
1500
|
+
const history: ChatMessage[] = [
|
|
1501
|
+
...active().messages,
|
|
1502
|
+
{ id: crypto.randomUUID(), role: 'user', parts },
|
|
1503
|
+
];
|
|
1504
|
+
setMessages(history);
|
|
1505
|
+
|
|
1506
|
+
// First real message names the conversation.
|
|
1507
|
+
if (active().title === 'New chat') {
|
|
1508
|
+
active().title = text
|
|
1509
|
+
? text.slice(0, 40)
|
|
1510
|
+
: (attachments[0]?.filename ?? 'Attachment');
|
|
1511
|
+
title.textContent = active().title;
|
|
1512
|
+
}
|
|
1513
|
+
renderRail();
|
|
1514
|
+
|
|
1515
|
+
setBusy(true);
|
|
1516
|
+
feedback.hidden = true;
|
|
1517
|
+
|
|
1518
|
+
// createAssistantStream appends the in-flight assistant message and folds
|
|
1519
|
+
// every delta onto the right part, handing back a fresh array each time.
|
|
1520
|
+
const stream = createAssistantStream((update) => setMessages(update(active().messages)));
|
|
1521
|
+
|
|
1522
|
+
try {
|
|
1523
|
+
const prompt = text || (attachments[0]?.filename ?? '');
|
|
1524
|
+
const result = await readOpenAIStream(mockResponse(prompt), stream);
|
|
1525
|
+
if (result.error) notify('Model error: ' + result.error.message, { variant: 'error' });
|
|
1526
|
+
|
|
1527
|
+
// The mock announces tool calls but nothing executes them, so the panel
|
|
1528
|
+
// would sit in \`input-available\` forever. Resolve each one here — the
|
|
1529
|
+
// same place a real app would put its tool result.
|
|
1530
|
+
for (const call of result.toolCalls) {
|
|
1531
|
+
stream.upsertTool(call.id, {
|
|
1532
|
+
state: 'output-available',
|
|
1533
|
+
output: {
|
|
1534
|
+
note: 'Local stub result — the mock responder announces the call, this app answers it.',
|
|
1535
|
+
query: call.input?.query ?? call.argumentsText,
|
|
1536
|
+
},
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
// abort() settles the message AND puts the reason where the user can see
|
|
1541
|
+
// it, instead of leaving a permanently blank bubble.
|
|
1542
|
+
stream.abort(
|
|
1543
|
+
error instanceof Error && error.message ? error.message : 'Request failed',
|
|
1544
|
+
);
|
|
1545
|
+
notify('The reply failed.', { variant: 'error' });
|
|
1546
|
+
console.error(error);
|
|
1547
|
+
} finally {
|
|
1548
|
+
stream.done(); // settles the turn; sink calls after this are dropped
|
|
1549
|
+
setBusy(false);
|
|
1550
|
+
feedback.hidden = false;
|
|
1551
|
+
renderRail();
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function setBusy(busy: boolean) {
|
|
1556
|
+
sending = busy;
|
|
1557
|
+
thread.loading = busy;
|
|
1558
|
+
composer.loading = busy;
|
|
1559
|
+
sendBtn.disabled = busy;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// ── message actions & feedback ────────────────────────────────────────────
|
|
1563
|
+
|
|
1564
|
+
thread.addEventListener('kai-message-action', (event) => {
|
|
1565
|
+
const { action, state } = (event as CustomEvent<{
|
|
1566
|
+
messageId: string;
|
|
1567
|
+
action: string;
|
|
1568
|
+
state?: 'on' | 'off';
|
|
1569
|
+
}>).detail;
|
|
1570
|
+
if (action === 'copy') notify('Copied to clipboard');
|
|
1571
|
+
else if (state === 'on') notify(action === 'like' ? 'Marked helpful' : 'Marked unhelpful');
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
feedback.categories = ['Wrong answer', 'Too slow', 'Missed my attachment', 'Other'];
|
|
1575
|
+
|
|
1576
|
+
feedback.addEventListener('kai-feedback', (event) => {
|
|
1577
|
+
const { value } = (event as CustomEvent<{ value: 'helpful' | 'not-helpful' }>).detail;
|
|
1578
|
+
if (value === 'helpful') notify('Thanks for the feedback', { variant: 'success' });
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
feedback.addEventListener('kai-feedback-detail', (event) => {
|
|
1582
|
+
const { category } = (event as CustomEvent<{ category?: string }>).detail;
|
|
1583
|
+
notify(category ? 'Feedback filed: ' + category : 'Feedback filed', {
|
|
1584
|
+
variant: 'success',
|
|
1585
|
+
});
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
feedback.addEventListener('kai-close', () => {
|
|
1589
|
+
feedback.hidden = true;
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
// ── new chat ──────────────────────────────────────────────────────────────
|
|
1593
|
+
|
|
1594
|
+
el<HTMLButtonElement>('new-chat').addEventListener('click', () => {
|
|
1595
|
+
const conversation: Conversation = {
|
|
1596
|
+
id: crypto.randomUUID(),
|
|
1597
|
+
title: 'New chat',
|
|
1598
|
+
messages: [],
|
|
1599
|
+
};
|
|
1600
|
+
conversations.unshift(conversation);
|
|
1601
|
+
select(conversation.id);
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
// ── boot ──────────────────────────────────────────────────────────────────
|
|
1605
|
+
|
|
1606
|
+
renderRail();
|
|
1607
|
+
renderStaged();
|
|
1608
|
+
select(activeId);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
void main();
|
|
1612
|
+
`
|
|
1613
|
+
},
|
|
1614
|
+
{
|
|
1615
|
+
path: "src/styles.css",
|
|
1616
|
+
lang: "css",
|
|
1617
|
+
code: `:root {
|
|
1618
|
+
color-scheme: light;
|
|
1619
|
+
--rail-w: 15rem;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
* { box-sizing: border-box; }
|
|
1623
|
+
|
|
1624
|
+
html, body { height: 100%; }
|
|
1625
|
+
|
|
1626
|
+
body {
|
|
1627
|
+
margin: 0;
|
|
1628
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
1629
|
+
background: var(--color-background, #fff);
|
|
1630
|
+
color: var(--color-foreground, #111);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
.app {
|
|
1634
|
+
position: fixed;
|
|
1635
|
+
inset: 0;
|
|
1636
|
+
display: grid;
|
|
1637
|
+
grid-template-columns: var(--rail-w) 1fr;
|
|
1638
|
+
text-align: start;
|
|
1639
|
+
/* Stays below the toast region's z-index (var(--kai-toast-z, 100)). */
|
|
1640
|
+
z-index: 90;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/* ── sidebar ─────────────────────────────────────────────── */
|
|
1644
|
+
|
|
1645
|
+
.rail {
|
|
1646
|
+
display: flex;
|
|
1647
|
+
flex-direction: column;
|
|
1648
|
+
min-height: 0;
|
|
1649
|
+
border-right: 1px solid var(--color-border, #e5e5e5);
|
|
1650
|
+
background: var(--color-muted, #fafafa);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
.rail-head {
|
|
1654
|
+
display: flex;
|
|
1655
|
+
align-items: center;
|
|
1656
|
+
justify-content: space-between;
|
|
1657
|
+
gap: 0.5rem;
|
|
1658
|
+
padding: 0.75rem 0.75rem 0.5rem;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
.rail-title {
|
|
1662
|
+
font-size: 0.75rem;
|
|
1663
|
+
font-weight: 600;
|
|
1664
|
+
letter-spacing: 0.04em;
|
|
1665
|
+
text-transform: uppercase;
|
|
1666
|
+
opacity: 0.6;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
.rail-list {
|
|
1670
|
+
flex: 1;
|
|
1671
|
+
min-height: 0;
|
|
1672
|
+
overflow-y: auto;
|
|
1673
|
+
padding: 0 0.5rem 0.75rem;
|
|
1674
|
+
display: flex;
|
|
1675
|
+
flex-direction: column;
|
|
1676
|
+
gap: 0.125rem;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
kai-conversation-item { display: block; }
|
|
1680
|
+
kai-conversation-item::part(row) { border-radius: 0.5rem; }
|
|
1681
|
+
|
|
1682
|
+
/* ── main pane ───────────────────────────────────────────── */
|
|
1683
|
+
|
|
1684
|
+
.pane {
|
|
1685
|
+
display: flex;
|
|
1686
|
+
flex-direction: column;
|
|
1687
|
+
min-height: 0;
|
|
1688
|
+
min-width: 0;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
.pane-head {
|
|
1692
|
+
display: flex;
|
|
1693
|
+
align-items: baseline;
|
|
1694
|
+
gap: 0.75rem;
|
|
1695
|
+
padding: 0.75rem 1rem;
|
|
1696
|
+
border-bottom: 1px solid var(--color-border, #e5e5e5);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
.pane-head h1 {
|
|
1700
|
+
margin: 0;
|
|
1701
|
+
font-size: 0.9375rem;
|
|
1702
|
+
font-weight: 600;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
.badge {
|
|
1706
|
+
font-size: 0.6875rem;
|
|
1707
|
+
padding: 0.125rem 0.5rem;
|
|
1708
|
+
border-radius: 999px;
|
|
1709
|
+
border: 1px solid var(--color-border, #e5e5e5);
|
|
1710
|
+
opacity: 0.7;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/* kai-thread fills the height its parent gives it and scrolls internally,
|
|
1714
|
+
so the pane is a flex column and the thread is the flexing row. */
|
|
1715
|
+
.thread {
|
|
1716
|
+
display: block;
|
|
1717
|
+
flex: 1;
|
|
1718
|
+
min-height: 0;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
.empty {
|
|
1722
|
+
display: flex;
|
|
1723
|
+
flex-direction: column;
|
|
1724
|
+
align-items: center;
|
|
1725
|
+
gap: 0.25rem;
|
|
1726
|
+
padding: 3rem 1rem;
|
|
1727
|
+
text-align: center;
|
|
1728
|
+
font-size: 0.875rem;
|
|
1729
|
+
opacity: 0.75;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
.empty p { margin: 0; }
|
|
1733
|
+
|
|
1734
|
+
.feedback {
|
|
1735
|
+
display: block;
|
|
1736
|
+
padding: 0 1rem 0.5rem;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
.feedback[hidden] { display: none; }
|
|
1740
|
+
|
|
1741
|
+
/* ── composer ────────────────────────────────────────────── */
|
|
1742
|
+
|
|
1743
|
+
.composer-bar {
|
|
1744
|
+
border-top: 1px solid var(--color-border, #e5e5e5);
|
|
1745
|
+
padding: 0.625rem 1rem 0.875rem;
|
|
1746
|
+
display: flex;
|
|
1747
|
+
flex-direction: column;
|
|
1748
|
+
gap: 0.5rem;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
.staged { display: block; }
|
|
1752
|
+
.staged[hidden] { display: none; }
|
|
1753
|
+
|
|
1754
|
+
.composer-row {
|
|
1755
|
+
display: flex;
|
|
1756
|
+
align-items: flex-end;
|
|
1757
|
+
gap: 0.5rem;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
.composer {
|
|
1761
|
+
display: block;
|
|
1762
|
+
flex: 1;
|
|
1763
|
+
min-width: 0;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
.icon-btn,
|
|
1767
|
+
.send-btn,
|
|
1768
|
+
.ghost-btn {
|
|
1769
|
+
font: inherit;
|
|
1770
|
+
cursor: pointer;
|
|
1771
|
+
border-radius: 0.5rem;
|
|
1772
|
+
border: 1px solid var(--color-border, #e5e5e5);
|
|
1773
|
+
background: var(--color-background, #fff);
|
|
1774
|
+
color: inherit;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.icon-btn {
|
|
1778
|
+
display: inline-flex;
|
|
1779
|
+
align-items: center;
|
|
1780
|
+
justify-content: center;
|
|
1781
|
+
width: 2.25rem;
|
|
1782
|
+
height: 2.25rem;
|
|
1783
|
+
flex: none;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
.send-btn {
|
|
1787
|
+
height: 2.25rem;
|
|
1788
|
+
padding: 0 0.875rem;
|
|
1789
|
+
flex: none;
|
|
1790
|
+
font-weight: 600;
|
|
1791
|
+
background: var(--color-primary, #111);
|
|
1792
|
+
color: var(--color-primary-foreground, #fff);
|
|
1793
|
+
border-color: transparent;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
.send-btn:disabled { opacity: 0.5; cursor: default; }
|
|
1797
|
+
|
|
1798
|
+
.ghost-btn {
|
|
1799
|
+
padding: 0.25rem 0.5rem;
|
|
1800
|
+
font-size: 0.75rem;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
.icon-btn:hover,
|
|
1804
|
+
.ghost-btn:hover { background: var(--color-accent, #f0f0f0); }
|
|
1805
|
+
`
|
|
1806
|
+
}
|
|
1807
|
+
]
|
|
1808
|
+
};
|
|
1809
|
+
const codeRecipes = [composedThread];
|
|
1810
|
+
function listCodeRecipes() {
|
|
1811
|
+
return codeRecipes;
|
|
1812
|
+
}
|
|
1813
|
+
function getCodeRecipe(id) {
|
|
1814
|
+
return codeRecipes.find((r) => r.id === id);
|
|
1815
|
+
}
|
|
1069
1816
|
const JS_ONLY_TYPE_PATTERNS = /\[\]|\{|Record</;
|
|
1070
1817
|
function isJsOnlyType(typeText) {
|
|
1071
1818
|
return typeText ? JS_ONLY_TYPE_PATTERNS.test(typeText) : false;
|
|
@@ -1255,8 +2002,8 @@ function catalogSectionLines(tag) {
|
|
|
1255
2002
|
// `renderRecipeAppendix` carries the notes exactly once.
|
|
1256
2003
|
"- **Wiring** — event out of A, property into B, wired by the host:"
|
|
1257
2004
|
);
|
|
1258
|
-
for (const
|
|
1259
|
-
lines.push(` - \`<${
|
|
2005
|
+
for (const w of r.wiring) {
|
|
2006
|
+
lines.push(` - \`<${w.from}>\` fires \`${w.event}\` → host sets \`${w.to}.${w.property}\``);
|
|
1260
2007
|
}
|
|
1261
2008
|
}
|
|
1262
2009
|
}
|
|
@@ -1279,9 +2026,9 @@ function renderRecipeAppendix() {
|
|
|
1279
2026
|
...compositionLines(r, " — "),
|
|
1280
2027
|
"- **Wiring** — event out of A, property into B, wired by the host:"
|
|
1281
2028
|
);
|
|
1282
|
-
for (const
|
|
2029
|
+
for (const w of r.wiring) {
|
|
1283
2030
|
lines.push(
|
|
1284
|
-
` - \`<${
|
|
2031
|
+
` - \`<${w.from}>\` fires \`${w.event}\` → host sets \`${w.to}.${w.property}\`` + (w.note ? ` — ${w.note}` : "")
|
|
1285
2032
|
);
|
|
1286
2033
|
}
|
|
1287
2034
|
}
|
|
@@ -1304,6 +2051,43 @@ function renderInvariantAppendix() {
|
|
|
1304
2051
|
}
|
|
1305
2052
|
return lines;
|
|
1306
2053
|
}
|
|
2054
|
+
const PROGRAMMATIC_ALIASES = ["programmatic", "state", "wire", "state-wire"];
|
|
2055
|
+
const PROGRAMMATIC_START = "<!-- kai:programmatic:start -->";
|
|
2056
|
+
const PROGRAMMATIC_END = "<!-- kai:programmatic:end -->";
|
|
2057
|
+
function renderProgrammaticAppendix() {
|
|
2058
|
+
const path = join(dirname(resolveManifestPath()), "..", "llms-full.txt");
|
|
2059
|
+
const missing = (what) => `Missing build artifact: ${what}
|
|
2060
|
+
|
|
2061
|
+
The programmatic-layer reference is generated from the shipped dist/*.d.ts by the build (a marker-fenced section of the package-root llms-full.txt). Run \`nx build ui\` (or \`npm run build:api\` in packages/ui) and ask again. It is deliberately not restated here: a hand-typed copy would drift from the declarations your editor shows.`;
|
|
2062
|
+
if (!existsSync(path)) return missing(path);
|
|
2063
|
+
const full = readFileSync(path, "utf-8");
|
|
2064
|
+
const start = full.indexOf(PROGRAMMATIC_START);
|
|
2065
|
+
const end = full.indexOf(PROGRAMMATIC_END);
|
|
2066
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
2067
|
+
return missing(`the ${PROGRAMMATIC_START} section inside ${path}`);
|
|
2068
|
+
}
|
|
2069
|
+
return full.slice(start + PROGRAMMATIC_START.length, end).trim();
|
|
2070
|
+
}
|
|
2071
|
+
function renderCodeRecipe(r) {
|
|
2072
|
+
const lines = [
|
|
2073
|
+
`## Recipe: ${r.title} (\`${r.id}\`)`,
|
|
2074
|
+
"",
|
|
2075
|
+
r.intent,
|
|
2076
|
+
"",
|
|
2077
|
+
`**Elements composed:** ${r.ingredients.map((t) => `\`<${t}>\``).join(", ")}`,
|
|
2078
|
+
"",
|
|
2079
|
+
"**Read these before pasting:**",
|
|
2080
|
+
...r.notes.map((n) => `- ${n}`)
|
|
2081
|
+
];
|
|
2082
|
+
for (const f2 of r.files) {
|
|
2083
|
+
lines.push("", `### \`${f2.path}\``, "", "```" + f2.lang, f2.code.trimEnd(), "```");
|
|
2084
|
+
}
|
|
2085
|
+
lines.push(
|
|
2086
|
+
"",
|
|
2087
|
+
`The wiring topology, invariants and composition claims behind this recipe are the surface recipe of the same id — call component_reference with { name: "recipes" }.`
|
|
2088
|
+
);
|
|
2089
|
+
return lines.join("\n");
|
|
2090
|
+
}
|
|
1307
2091
|
function eventDetail(typeText) {
|
|
1308
2092
|
const m = /^CustomEvent<([\s\S]*)>$/.exec(typeText?.trim() ?? "");
|
|
1309
2093
|
return m ? m[1].trim() || void 0 : void 0;
|
|
@@ -1458,10 +2242,10 @@ Call component_reference with no name (or name: "list") to list all ${all.length
|
|
|
1458
2242
|
}
|
|
1459
2243
|
const reference = {
|
|
1460
2244
|
name: "component_reference",
|
|
1461
|
-
description:
|
|
2245
|
+
description: 'Look up AI/UI (kai-*) web components: their tags, props, events, imperative methods, and usage examples. For a card-backed element it also returns the card\'s JSON Schema and a ready-to-send tool definition generated from it — pass `provider` to get that provider\'s envelope. Also serves the programmatic layer (name: "programmatic" — the @kitn.ai/ui/state + @kitn.ai/ui/wire streaming/mock/encode API) and complete code recipes (e.g. name: "composed-thread" — a hand-composed thread with no <kai-chat>).',
|
|
1462
2246
|
inputSchema: z$1.object({
|
|
1463
2247
|
name: z$1.string().optional().describe(
|
|
1464
|
-
'The element tag, e.g. "kai-chat". Omit it (or pass "list") to get the index of every element with a one-line summary, then ask again for the one you want. Pass "invariants" for the full diagnosis and wrong/right examples behind every invariant id an element lookup shows. Pass "recipes" for the full wiring notes behind every surface recipe an element lookup names.'
|
|
2248
|
+
'The element tag, e.g. "kai-chat". Omit it (or pass "list") to get the index of every element with a one-line summary, then ask again for the one you want. Pass "invariants" for the full diagnosis and wrong/right examples behind every invariant id an element lookup shows. Pass "recipes" for the full wiring notes behind every surface recipe an element lookup names. Pass "programmatic" (or "state" / "wire") for the @kitn.ai/ui/state + @kitn.ai/ui/wire API — streaming folds, the mock responder, the SSE readers/encoders. Pass a code-recipe id (e.g. "composed-thread") for a complete pasteable composition.'
|
|
1465
2249
|
),
|
|
1466
2250
|
provider: z$1.enum(PROVIDERS).optional().describe(
|
|
1467
2251
|
`Which tool-definition envelope to project for a card-backed element. Defaults to "${DEFAULT_PROVIDER}", the provider-neutral { name, description, schema } form.`
|
|
@@ -1500,11 +2284,19 @@ The elements that HOST them (and carry the \`cardTypes\` / \`cardSchemas\` props
|
|
|
1500
2284
|
|
|
1501
2285
|
Call component_reference with { name: "invariants" } for the full diagnosis and wrong/right examples behind every invariant id shown on an element lookup.
|
|
1502
2286
|
|
|
1503
|
-
Call component_reference with { name: "recipes" } for the full wiring notes behind every surface recipe an element lookup names
|
|
2287
|
+
Call component_reference with { name: "recipes" } for the full wiring notes behind every surface recipe an element lookup names.
|
|
2288
|
+
|
|
2289
|
+
Call component_reference with { name: "programmatic" } for the @kitn.ai/ui/state + @kitn.ai/ui/wire API — createAssistantStream, the part folds, createMockResponder (and its scripted tool calls), the SSE readers and the provider encoders. That layer is what a host wires when it composes elements by hand instead of using <kai-chat>.
|
|
2290
|
+
|
|
2291
|
+
Complete pasteable compositions (code recipes): ${listCodeRecipes().map((r) => `{ name: "${r.id}" } — ${r.title}`).join("; ")}.`;
|
|
1504
2292
|
} else if (name === "invariants") {
|
|
1505
2293
|
text2 = renderInvariantAppendix().join("\n");
|
|
1506
2294
|
} else if (name === "recipes") {
|
|
1507
2295
|
text2 = renderRecipeAppendix().join("\n");
|
|
2296
|
+
} else if (PROGRAMMATIC_ALIASES.includes(name)) {
|
|
2297
|
+
text2 = renderProgrammaticAppendix();
|
|
2298
|
+
} else if (getCodeRecipe(name)) {
|
|
2299
|
+
text2 = renderCodeRecipe(getCodeRecipe(name));
|
|
1508
2300
|
} else {
|
|
1509
2301
|
text2 = formatReference(name, provider);
|
|
1510
2302
|
}
|
|
@@ -4541,8 +5333,8 @@ function slottedInChat(components) {
|
|
|
4541
5333
|
});
|
|
4542
5334
|
}
|
|
4543
5335
|
function railSiblingNote(tag, placement) {
|
|
4544
|
-
const
|
|
4545
|
-
const handler = `on${toPascalCase(
|
|
5336
|
+
const w = conversationsWiring();
|
|
5337
|
+
const handler = `on${toPascalCase(w.event)}`;
|
|
4546
5338
|
return [
|
|
4547
5339
|
` {/* NOT WIRED, and NOT SLOTTED — both deliberate, both fixable here.`,
|
|
4548
5340
|
` COMPOSITION: the kit composes this as <${tag} slot="${placement.slot}"> INSIDE`,
|
|
@@ -4550,7 +5342,7 @@ function railSiblingNote(tag, placement) {
|
|
|
4550
5342
|
` The generated React wrappers take no \`slot\` prop (see WebComponentProps`,
|
|
4551
5343
|
` in @kitn.ai/ui/react), so this renders as a sibling instead. Drop to the`,
|
|
4552
5344
|
` raw <${placement.parent}> custom element if you want the slotted layout.`,
|
|
4553
|
-
` WIRING: set ${
|
|
5345
|
+
` WIRING: set ${w.property}={...} and ${handler}={...} — the handler`,
|
|
4554
5346
|
` reads event.detail.id and calls setMessages with that thread. */}`
|
|
4555
5347
|
];
|
|
4556
5348
|
}
|
|
@@ -4560,7 +5352,7 @@ function htmlComment(lines, pad) {
|
|
|
4560
5352
|
);
|
|
4561
5353
|
}
|
|
4562
5354
|
function slottedChildMarkup(components, pad, opts) {
|
|
4563
|
-
const
|
|
5355
|
+
const w = conversationsWiring();
|
|
4564
5356
|
return slottedInChat(components).flatMap((s) => [
|
|
4565
5357
|
// What the shell DOES, and — the half a comment gets wrong by being generous
|
|
4566
5358
|
// — what it does not. `<kai-chat>`'s sidebar region is one fixed-width aside
|
|
@@ -4577,7 +5369,7 @@ function slottedChildMarkup(components, pad, opts) {
|
|
|
4577
5369
|
],
|
|
4578
5370
|
pad
|
|
4579
5371
|
),
|
|
4580
|
-
...opts.wired ? [] : htmlComment(notWiredNote(s.tag,
|
|
5372
|
+
...opts.wired ? [] : htmlComment(notWiredNote(s.tag, w.property, w.event), pad),
|
|
4581
5373
|
`${pad}<${s.tag}${opts.ids ? ` id="${s.tag.replace(/^kai-/, "")}"` : ""} slot="${s.slot}" style="display:block;height:100%"></${s.tag}>`
|
|
4582
5374
|
]);
|
|
4583
5375
|
}
|
|
@@ -4590,7 +5382,7 @@ function notWiredNote(tag, property, event) {
|
|
|
4590
5382
|
];
|
|
4591
5383
|
}
|
|
4592
5384
|
function conversationsWiring() {
|
|
4593
|
-
const edge = surfaceRecipes.flatMap((r) => r.wiring).find((
|
|
5385
|
+
const edge = surfaceRecipes.flatMap((r) => r.wiring).find((w) => w.from === "kai-conversations" && w.to === "kai-chat" && w.property === "messages");
|
|
4594
5386
|
return { property: "conversations", event: edge?.event ?? "kai-conversation-select" };
|
|
4595
5387
|
}
|
|
4596
5388
|
const ATTACHMENT_TAGS = /* @__PURE__ */ new Set(["kai-file-upload", "kai-attachments"]);
|
|
@@ -8205,7 +8997,7 @@ const scaffold = {
|
|
|
8205
8997
|
);
|
|
8206
8998
|
}
|
|
8207
8999
|
};
|
|
8208
|
-
const themeCss = "/* @kitn.ai/ui design system — import after `@import \"tailwindcss\"`.\n Defines ONLY namespaced (--color-*) tokens, component keyframes, animation\n utilities, and the .chat-markdown styles. Contains NO global base/reset rules,\n so importing it restyles nothing on its own — a host opts in via the kit's\n components + classes. Rebrand by overriding the --color-* values (optionally\n scoped to a subtree).\n NOTE: This file is Tailwind-v4 source (requires tailwindcss as a peer). For\n bundler-based apps that don't use Tailwind, import @kitn.ai/ui/theme.tokens.css\n instead — it is plain compiled CSS with no toolchain requirements. */\n\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n --color-background: var(--kai-color-background, hsl(0 0% 100%));\n --color-foreground: var(--kai-color-foreground, hsl(240 10% 3.9%));\n --color-card: var(--kai-color-card, hsl(0 0% 100%));\n --color-card-foreground: var(--kai-color-card-foreground, hsl(240 10% 3.9%));\n --color-popover: var(--kai-color-popover, hsl(0 0% 100%));\n --color-popover-foreground: var(--kai-color-popover-foreground, hsl(240 10% 3.9%));\n --color-primary: var(--kai-color-primary, hsl(240 5.9% 10%));\n --color-primary-foreground: var(--kai-color-primary-foreground, hsl(0 0% 98%));\n --color-secondary: var(--kai-color-secondary, hsl(240 4.8% 95.9%));\n --color-secondary-foreground: var(--kai-color-secondary-foreground, hsl(240 5.9% 10%));\n --color-muted: var(--kai-color-muted, hsl(240 4.8% 95.9%));\n --color-muted-foreground: var(--kai-color-muted-foreground, hsl(240 3.8% 43%));\n /* Subtle SURFACE — an opaque \"muted panel\" for things that sit OVER content\n (the prompt input, message bubbles, artifact/card headers, chips). CALCULATED\n from existing tokens — `--color-muted` blended 40% onto `--color-background`\n — so it tracks the theme (incl. dark) automatically and needs no hand-picked\n value. SOLID (no alpha), so nothing behind it bleeds through. This replaces\n ad-hoc `bg-muted/40` surfaces. Retint via the --kai-color-surface knob (or a\n bare value); --color-surface-strong is the stronger step (hover/raised) and\n --color-surface-sunken is recessed BELOW the base surface (wells, or a banner\n that sits behind a card). Sunken is theme-specific: \"darker than the card\"\n means a LOWER muted% in dark but a HIGHER one in light, so the two scopes\n can't share a number — see the .dark block. */\n --color-surface: var(--kai-color-surface, color-mix(in srgb, var(--color-muted) 40%, var(--color-background)));\n --color-surface-strong: var(--kai-color-surface-strong, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)));\n --color-surface-sunken: var(--kai-color-surface-sunken, color-mix(in srgb, var(--color-muted) 85%, var(--color-background)));\n --color-accent: var(--kai-color-accent, hsl(240 4.8% 95.9%));\n --color-accent-foreground: var(--kai-color-accent-foreground, hsl(240 5.9% 10%));\n --color-destructive: var(--kai-color-destructive, hsl(0 72% 45%));\n --color-destructive-foreground: var(--kai-color-destructive-foreground, hsl(0 0% 98%));\n /* Semantic status colors - solid fills with a legible foreground, matched to\n the kit's tool-green / -amber / -blue hues so they read on-brand. Use\n bg-success / bg-warning / bg-info with the matching -foreground for badges,\n banners, and buttons; or text-success / -warning / -info on a soft tint\n (below). destructive (above) is the danger/error member of the same family.\n Light uses darker hues so the color carries a light foreground AND stays\n legible as text on a pale tint; dark (see .dark) flips to brighter hues over\n a near-black foreground. Override via --kai-color-success / -warning / -info\n (+ -foreground). */\n --color-success: var(--kai-color-success, hsl(142 71% 30%));\n --color-success-foreground: var(--kai-color-success-foreground, hsl(0 0% 98%));\n --color-warning: var(--kai-color-warning, hsl(38 92% 30%));\n --color-warning-foreground: var(--kai-color-warning-foreground, hsl(0 0% 98%));\n --color-info: var(--kai-color-info, hsl(217 91% 42%));\n --color-info-foreground: var(--kai-color-info-foreground, hsl(0 0% 98%));\n /* Soft tinted status surfaces - OPAQUE, theme-tracking fills derived as ~14% of\n the status hue blended onto the background. Use these for subtle callouts /\n banners instead of an eyeballed bg-success/10; pair with text-success /\n -warning / -info / -destructive. Derived, so declared in BOTH light and dark\n (a color-mix resolves where it's declared - see the surface tokens above). A\n bare bg- utility for a color-mix token is added at the bottom (see bg-surface). */\n --color-success-soft: var(--kai-color-success-soft, color-mix(in oklab, var(--color-success) 14%, var(--color-background)));\n --color-warning-soft: var(--kai-color-warning-soft, color-mix(in oklab, var(--color-warning) 14%, var(--color-background)));\n --color-info-soft: var(--kai-color-info-soft, color-mix(in oklab, var(--color-info) 14%, var(--color-background)));\n --color-destructive-soft: var(--kai-color-destructive-soft, color-mix(in oklab, var(--color-destructive) 14%, var(--color-background)));\n /* Interaction-state backgrounds - opaque, theme-tracking fills for row/control\n states so consumers stop eyeballing /10 opacities. --color-hover is a subtle\n neutral (a little --color-muted on the background); --color-selected is a\n faint --color-primary tint so a selected row tracks the brand. Use bg-hover /\n bg-selected. Derived, so declared in BOTH light and dark; bare bg- utilities\n added at the bottom. */\n --color-hover: var(--kai-color-hover, color-mix(in oklab, var(--color-muted) 60%, var(--color-background)));\n --color-selected: var(--kai-color-selected, color-mix(in oklab, var(--color-primary) 8%, var(--color-background)));\n --color-border: var(--kai-color-border, hsl(240 5.9% 90%));\n --color-input: var(--kai-color-input, hsl(240 5.9% 90%));\n /* Focus ring — a deliberate blue (not a neutral) so keyboard focus is always\n obvious and on-brand in both modes. Light uses a strong blue; dark uses a\n brighter one for contrast on dark surfaces. Both clear WCAG 2.1 non-text\n contrast (≥3:1) against the kit's backgrounds. Override via --kai-color-ring. */\n --color-ring: var(--kai-color-ring, hsl(217 91% 53%));\n /* Custom scrollbars — thin, rounded, subtle; thumb strengthens on hover. A\n consistent cross-platform look (the kit styles its OWN scroll regions inside\n the shadow root; the track stays transparent so nothing is forced where no\n scrollbar is needed). Override via --kai-color-scrollbar-thumb(-hover). */\n --color-scrollbar-thumb: var(--kai-color-scrollbar-thumb, hsl(240 5% 80%));\n --color-scrollbar-thumb-hover: var(--kai-color-scrollbar-thumb-hover, hsl(240 4% 64%));\n --color-sidebar: var(--kai-color-sidebar, hsl(0 0% 100%));\n\n /* Inline `code` accent. Blue text on a translucent blue chip. */\n --color-code-foreground: var(--kai-color-code-foreground, hsl(224.3 76.3% 48%));\n\n /* Tool/status chip hues. Each chip is hue text over a 15% translucent fill of\n the SAME hue (set in tool.tsx). The bare hue is too light to reach WCAG AA\n (4.5:1) on the faint fill in LIGHT mode, so the light values are darkened;\n dark mode keeps brighter hues for AA on the dark fill. Override via\n --kai-color-tool-* to retheme. */\n --color-tool-blue: var(--kai-color-tool-blue, hsl(217 91% 38%));\n --color-tool-amber: var(--kai-color-tool-amber, hsl(38 92% 28%));\n --color-tool-green: var(--kai-color-tool-green, hsl(142 71% 26%));\n --color-tool-red: var(--kai-color-tool-red, hsl(0 72% 42%));\n\n --radius: var(--kai-radius, 0.6rem);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n\n /* Typography scale — semantic sizes shared by all components. Each generates a\n Tailwind utility (text-caption / text-meta / text-body / text-title). To\n restyle the kit's typography, override the namespaced --kai-text-* token on\n :root (e.g. `--kai-text-body: 0.9375rem`) — it pierces the Shadow DOM via the\n var() fallback, exactly like the --kai-color-* tokens. The bare --text-*\n names stay internal so a host's own --text-* can't collide. (Message/markdown/\n input reading size also scales with the `proseSize` prop; these tokens cover\n the fixed chrome & controls.) */\n --text-caption: var(--kai-text-caption, 0.6875rem); --text-caption--line-height: 1rem; /* 11px — micro labels, badges, sub-counts */\n --text-meta: var(--kai-text-meta, 0.75rem); --text-meta--line-height: 1.1rem; /* 12px — controls, toggles, switchers, captions */\n --text-body: var(--kai-text-body, 0.875rem); --text-body--line-height: 1.45rem; /* 14px — primary reading text */\n --text-title: var(--kai-text-title, 1rem); --text-title--line-height: 1.5rem; /* 16px — emphasis / headers */\n\n @keyframes typing { 0%,100% { transform: translateY(0); opacity: .5 } 50% { transform: translateY(-2px); opacity: 1 } }\n @keyframes loading-dots { 0%,100% { opacity: 0 } 50% { opacity: 1 } }\n @keyframes wave { 0%,100% { transform: scaleY(1) } 50% { transform: scaleY(.6) } }\n @keyframes blink { 0%,100% { opacity: 1 } 50% { opacity: 0 } }\n @keyframes text-blink { 0%,100% { color: var(--color-primary) } 50% { color: var(--color-muted-foreground) } }\n @keyframes bounce-dots { 0%,100% { transform: scale(.8); opacity: .5 } 50% { transform: scale(1.2); opacity: 1 } }\n @keyframes thin-pulse { 0%,100% { transform: scale(.95); opacity: .8 } 50% { transform: scale(1.05); opacity: .4 } }\n @keyframes pulse-dot { 0%,100% { transform: scale(1); opacity: .8 } 50% { transform: scale(1.5); opacity: 1 } }\n @keyframes shimmer-text { 0% { background-position: 150% center } 100% { background-position: -150% center } }\n @keyframes wave-bars { 0%,100% { transform: scaleY(1); opacity: .5 } 50% { transform: scaleY(.6); opacity: 1 } }\n @keyframes shimmer { 0% { background-position: 200% 50% } 100% { background-position: -200% 50% } }\n @keyframes spinner-fade { 0% { opacity: 0 } 100% { opacity: 1 } }\n @keyframes collapsible-down { from { height: 0; opacity: 0 } to { height: var(--kb-collapsible-content-height); opacity: 1 } }\n @keyframes collapsible-up { from { height: var(--kb-collapsible-content-height); opacity: 1 } to { height: 0; opacity: 0 } }\n}\n\n.dark {\n --color-background: var(--kai-color-background, hsl(50 2% 9%));\n --color-foreground: var(--kai-color-foreground, hsl(0 0% 98%));\n --color-card: var(--kai-color-card, hsl(45 4% 12%));\n --color-card-foreground: var(--kai-color-card-foreground, hsl(0 0% 98%));\n --color-popover: var(--kai-color-popover, hsl(45 4% 12%));\n --color-popover-foreground: var(--kai-color-popover-foreground, hsl(0 0% 98%));\n --color-primary: var(--kai-color-primary, hsl(0 0% 98%));\n --color-primary-foreground: var(--kai-color-primary-foreground, hsl(45 4% 11%));\n --color-secondary: var(--kai-color-secondary, hsl(45 4% 17%));\n --color-secondary-foreground: var(--kai-color-secondary-foreground, hsl(0 0% 98%));\n --color-muted: var(--kai-color-muted, hsl(45 4% 17%));\n --color-muted-foreground: var(--kai-color-muted-foreground, hsl(45 4% 64%));\n /* Re-declared in the dark scope so the color-mix recomputes with the DARK\n muted/background (a derived token is computed where it's declared, not where\n it's used — see the light --color-surface for the full note). */\n --color-surface: var(--kai-color-surface, color-mix(in srgb, var(--color-muted) 40%, var(--color-background)));\n --color-surface-strong: var(--kai-color-surface-strong, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)));\n /* Sunken: in dark a LOWER muted% is darker (blends toward the dark background). */\n --color-surface-sunken: var(--kai-color-surface-sunken, color-mix(in srgb, var(--color-muted) 18%, var(--color-background)));\n --color-accent: var(--kai-color-accent, hsl(45 4% 17%));\n --color-accent-foreground: var(--kai-color-accent-foreground, hsl(0 0% 98%));\n --color-destructive: var(--kai-color-destructive, hsl(0 62.8% 30.6%));\n --color-destructive-foreground: var(--kai-color-destructive-foreground, hsl(0 0% 98%));\n /* Semantic status colors - dark mode flips to the brighter tool-* dark hues\n (matching the chip values) carried over a near-black foreground, so the color\n both reads as text on a dark soft tint AND backs a legible foreground. See\n the light @theme note. */\n --color-success: var(--kai-color-success, hsl(142 71% 45%));\n --color-success-foreground: var(--kai-color-success-foreground, hsl(45 4% 11%));\n --color-warning: var(--kai-color-warning, hsl(38 92% 50%));\n --color-warning-foreground: var(--kai-color-warning-foreground, hsl(45 4% 11%));\n --color-info: var(--kai-color-info, hsl(217 91% 70%));\n --color-info-foreground: var(--kai-color-info-foreground, hsl(45 4% 11%));\n /* Soft status surfaces + interaction states - re-declared so each color-mix\n recomputes with the DARK hues/background (a derived token resolves where it's\n declared; see the light block for the full note). */\n --color-success-soft: var(--kai-color-success-soft, color-mix(in oklab, var(--color-success) 14%, var(--color-background)));\n --color-warning-soft: var(--kai-color-warning-soft, color-mix(in oklab, var(--color-warning) 14%, var(--color-background)));\n --color-info-soft: var(--kai-color-info-soft, color-mix(in oklab, var(--color-info) 14%, var(--color-background)));\n --color-destructive-soft: var(--kai-color-destructive-soft, color-mix(in oklab, var(--color-destructive) 14%, var(--color-background)));\n --color-hover: var(--kai-color-hover, color-mix(in oklab, var(--color-muted) 60%, var(--color-background)));\n --color-selected: var(--kai-color-selected, color-mix(in oklab, var(--color-primary) 8%, var(--color-background)));\n --color-border: var(--kai-color-border, hsl(45 4% 17%));\n --color-input: var(--kai-color-input, hsl(45 4% 17%));\n --color-ring: var(--kai-color-ring, hsl(217 91% 68%));\n --color-scrollbar-thumb: var(--kai-color-scrollbar-thumb, hsl(45 3% 30%));\n --color-scrollbar-thumb-hover: var(--kai-color-scrollbar-thumb-hover, hsl(45 3% 42%));\n --color-sidebar: var(--kai-color-sidebar, hsl(50 2% 7%));\n --color-code-foreground: var(--kai-color-code-foreground, hsl(213 94% 78%));\n\n /* Tool/status chip hues — dark mode. Brighter hues reach AA on the dark fill. */\n --color-tool-blue: var(--kai-color-tool-blue, hsl(217 91% 70%));\n --color-tool-amber: var(--kai-color-tool-amber, hsl(38 92% 50%));\n --color-tool-green: var(--kai-color-tool-green, hsl(142 71% 45%));\n --color-tool-red: var(--kai-color-tool-red, hsl(0 84% 70%));\n}\n\n/* Self-contained markdown styling — replaces the typography plugin's `prose`.\n Keyed on --color-* tokens so it themes with the rest of the kit. */\n.chat-markdown { color: var(--color-foreground); line-height: 1.6; }\n.chat-markdown > div:first-child > :first-child { margin-top: 0; }\n.chat-markdown > div:last-child > :last-child { margin-bottom: 0; }\n.chat-markdown p { margin: 0 0 0.75em; }\n.chat-markdown p:last-child { margin-bottom: 0; }\n.chat-markdown h1, .chat-markdown h2, .chat-markdown h3, .chat-markdown h4 {\n margin: 1.2em 0 0.5em; font-weight: 600; line-height: 1.3; color: var(--color-foreground);\n}\n.chat-markdown h1 { font-size: 1.4em; } .chat-markdown h2 { font-size: 1.25em; }\n.chat-markdown h3 { font-size: 1.1em; } .chat-markdown h4 { font-size: 1em; }\n.chat-markdown ul, .chat-markdown ol { margin: 0 0 0.75em; padding-left: 1.4em; }\n.chat-markdown li { margin: 0.2em 0; }\n.chat-markdown ul { list-style: disc; } .chat-markdown ol { list-style: decimal; }\n.chat-markdown a { color: var(--color-primary); text-decoration: underline; text-underline-offset: 2px; }\n.chat-markdown strong { font-weight: 600; color: var(--color-foreground); }\n.chat-markdown em { font-style: italic; }\n.chat-markdown blockquote {\n margin: 0 0 0.75em; padding-left: 0.9em; border-left: 2px solid var(--color-border);\n color: var(--color-muted-foreground);\n}\n.chat-markdown hr { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border); }\n.chat-markdown code:not(pre code) {\n background: color-mix(in oklab, var(--color-code-foreground) 15%, transparent);\n color: var(--color-code-foreground);\n border-radius: 4px; padding: 0.1em 0.35em; font-size: 0.875em;\n font-family: var(--kai-font-code, ui-monospace, \"SF Mono\", Menlo, monospace);\n}\n.chat-markdown table { width: 100%; border-collapse: collapse; margin: 0 0 0.75em; font-size: 0.9em; }\n.chat-markdown th, .chat-markdown td { border: 1px solid var(--color-border); padding: 0.4em 0.6em; text-align: left; }\n.chat-markdown th { font-weight: 600; background: color-mix(in oklab, var(--color-muted-foreground) 8%, transparent); }\n\n/* Themeable elevation. The kit is mostly flat (borders), but a few surfaces lift\n off the page — popovers, dropdowns, hover cards, cards. The shadow COLOR is a\n token (--kai-shadow-color, default near-black); set it transparent for a fully\n flat look, or tint it to match a theme. Defined here (not as a Tailwind shadow\n utility) so the color stays overridable and works in both the web-component and\n SolidJS layers. */\n.kai-elevation {\n box-shadow:\n 0 4px 12px -2px color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 14%, transparent),\n 0 2px 6px -2px color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 10%, transparent);\n}\n.kai-elevation-sm {\n box-shadow: 0 1px 2px 0 color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 8%, transparent);\n}\n\n/* Cross-platform thin scrollbar utility (Firefox + WebKit). Used by ScrollArea\n and any scroll region in the SolidJS-component build (web components also get\n this globally inside their shadow root — see src/elements/styles.css). */\n.scrollbar-thin {\n scrollbar-width: thin;\n scrollbar-color: var(--color-scrollbar-thumb) transparent;\n}\n.scrollbar-thin::-webkit-scrollbar { width: 8px; height: 8px; }\n.scrollbar-thin::-webkit-scrollbar-track { background: transparent; }\n.scrollbar-thin::-webkit-scrollbar-thumb {\n background-color: var(--color-scrollbar-thumb);\n border-radius: 9999px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n.scrollbar-thin::-webkit-scrollbar-thumb:hover { background-color: var(--color-scrollbar-thumb-hover); }\n.scrollbar-thin::-webkit-scrollbar-corner { background: transparent; }\n\n/* Surface utilities — `bg-surface` / `bg-surface-strong` paint the opaque,\n theme-derived surface tokens (see --color-surface above). Defined explicitly\n because a color-mix-valued @theme token does NOT emit a bare `bg-` utility on\n its own (only opacity variants). Use these for panels that sit OVER content\n instead of `bg-muted/NN`. */\n@utility bg-surface {\n background-color: var(--color-surface);\n}\n@utility bg-surface-strong {\n background-color: var(--color-surface-strong);\n}\n@utility bg-surface-sunken {\n background-color: var(--color-surface-sunken);\n}\n\n/* Soft-status + interaction-state utilities. Same reason as bg-surface above: a\n color-mix-valued @theme token does NOT emit a bare `bg-` utility on its own, so\n bg-success-soft / bg-warning-soft / bg-info-soft / bg-destructive-soft and\n bg-hover / bg-selected are declared here. (The SOLID tokens - success / warning\n / info + their -foreground - are plain hsl values, so bg-success / text-success\n etc. ARE auto-generated by @theme and need no explicit utility.) */\n@utility bg-success-soft {\n background-color: var(--color-success-soft);\n}\n@utility bg-warning-soft {\n background-color: var(--color-warning-soft);\n}\n@utility bg-info-soft {\n background-color: var(--color-info-soft);\n}\n@utility bg-destructive-soft {\n background-color: var(--color-destructive-soft);\n}\n@utility bg-hover {\n background-color: var(--color-hover);\n}\n@utility bg-selected {\n background-color: var(--color-selected);\n}\n";
|
|
9000
|
+
const themeCss = "/* @kitn.ai/ui design system — import after `@import \"tailwindcss\"`.\n Defines ONLY namespaced (--color-*) tokens, component keyframes, animation\n utilities, and the .chat-markdown styles. Contains NO global base/reset rules,\n so importing it restyles nothing on its own — a host opts in via the kit's\n components + classes. Rebrand by overriding the --color-* values (optionally\n scoped to a subtree).\n NOTE: This file is Tailwind-v4 source (requires tailwindcss as a peer). For\n bundler-based apps that don't use Tailwind, import @kitn.ai/ui/theme.tokens.css\n instead — it is plain compiled CSS with no toolchain requirements. */\n\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n --color-background: var(--kai-color-background, hsl(0 0% 100%));\n --color-foreground: var(--kai-color-foreground, hsl(240 10% 3.9%));\n --color-card: var(--kai-color-card, hsl(0 0% 100%));\n --color-card-foreground: var(--kai-color-card-foreground, hsl(240 10% 3.9%));\n --color-popover: var(--kai-color-popover, hsl(0 0% 100%));\n --color-popover-foreground: var(--kai-color-popover-foreground, hsl(240 10% 3.9%));\n --color-primary: var(--kai-color-primary, hsl(240 5.9% 10%));\n --color-primary-foreground: var(--kai-color-primary-foreground, hsl(0 0% 98%));\n --color-secondary: var(--kai-color-secondary, hsl(240 4.8% 95.9%));\n --color-secondary-foreground: var(--kai-color-secondary-foreground, hsl(240 5.9% 10%));\n --color-muted: var(--kai-color-muted, hsl(240 4.8% 95.9%));\n --color-muted-foreground: var(--kai-color-muted-foreground, hsl(240 3.8% 43%));\n /* Subtle SURFACE — an opaque \"muted panel\" for things that sit OVER content\n (the prompt input, message bubbles, artifact/card headers, chips). CALCULATED\n from existing tokens — `--color-muted` blended 40% onto `--color-background`\n — so it tracks the theme (incl. dark) automatically and needs no hand-picked\n value. SOLID (no alpha), so nothing behind it bleeds through. This replaces\n ad-hoc `bg-muted/40` surfaces. Retint via the --kai-color-surface knob (or a\n bare value); --color-surface-strong is the stronger step (hover/raised) and\n --color-surface-sunken is recessed BELOW the base surface (wells, or a banner\n that sits behind a card). Sunken is theme-specific: \"darker than the card\"\n means a LOWER muted% in dark but a HIGHER one in light, so the two scopes\n can't share a number — see the .dark block. */\n --color-surface: var(--kai-color-surface, color-mix(in srgb, var(--color-muted) 40%, var(--color-background)));\n --color-surface-strong: var(--kai-color-surface-strong, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)));\n --color-surface-sunken: var(--kai-color-surface-sunken, color-mix(in srgb, var(--color-muted) 85%, var(--color-background)));\n --color-accent: var(--kai-color-accent, hsl(240 4.8% 95.9%));\n --color-accent-foreground: var(--kai-color-accent-foreground, hsl(240 5.9% 10%));\n --color-destructive: var(--kai-color-destructive, hsl(0 72% 45%));\n --color-destructive-foreground: var(--kai-color-destructive-foreground, hsl(0 0% 98%));\n /* Semantic status colors - solid fills with a legible foreground, matched to\n the kit's tool-green / -amber / -blue hues so they read on-brand. Use\n bg-success / bg-warning / bg-info with the matching -foreground for badges,\n banners, and buttons; or text-success / -warning / -info on a soft tint\n (below). destructive (above) is the danger/error member of the same family.\n Light uses darker hues so the color carries a light foreground AND stays\n legible as text on a pale tint; dark (see .dark) flips to brighter hues over\n a near-black foreground. Override via --kai-color-success / -warning / -info\n (+ -foreground). */\n --color-success: var(--kai-color-success, hsl(142 71% 30%));\n --color-success-foreground: var(--kai-color-success-foreground, hsl(0 0% 98%));\n --color-warning: var(--kai-color-warning, hsl(38 92% 30%));\n --color-warning-foreground: var(--kai-color-warning-foreground, hsl(0 0% 98%));\n --color-info: var(--kai-color-info, hsl(217 91% 42%));\n --color-info-foreground: var(--kai-color-info-foreground, hsl(0 0% 98%));\n /* Soft tinted status surfaces - OPAQUE, theme-tracking fills derived as ~14% of\n the status hue blended onto the background. Use these for subtle callouts /\n banners instead of an eyeballed bg-success/10; pair with text-success /\n -warning / -info / -destructive. Derived, so declared in BOTH light and dark\n (a color-mix resolves where it's declared - see the surface tokens above). A\n bare bg- utility for a color-mix token is added at the bottom (see bg-surface). */\n --color-success-soft: var(--kai-color-success-soft, color-mix(in oklab, var(--color-success) 14%, var(--color-background)));\n --color-warning-soft: var(--kai-color-warning-soft, color-mix(in oklab, var(--color-warning) 14%, var(--color-background)));\n --color-info-soft: var(--kai-color-info-soft, color-mix(in oklab, var(--color-info) 14%, var(--color-background)));\n --color-destructive-soft: var(--kai-color-destructive-soft, color-mix(in oklab, var(--color-destructive) 14%, var(--color-background)));\n /* Interaction-state backgrounds - opaque, theme-tracking fills for row/control\n states so consumers stop eyeballing /10 opacities. --color-hover is a subtle\n neutral (a little --color-muted on the background); --color-selected is a\n faint --color-primary tint so a selected row tracks the brand. Use bg-hover /\n bg-selected. Derived, so declared in BOTH light and dark; bare bg- utilities\n added at the bottom. */\n --color-hover: var(--kai-color-hover, color-mix(in oklab, var(--color-muted) 60%, var(--color-background)));\n --color-selected: var(--kai-color-selected, color-mix(in oklab, var(--color-primary) 8%, var(--color-background)));\n /* MARKED text — the fill behind a keyword the app has flagged (the composer's\n `highlights` rules, decorated through the CSS Custom Highlight API). This is\n `<mark>`, not a selection and not a status: a warm yellow, the convention\n every browser already ships for <mark> and every reader already reads as\n \"someone marked this\". Deliberately NOT derived from --color-primary — that\n token is a NEUTRAL near-black in light and near-white in dark, so any tint of\n it is grey, which is why the highlight used to be invisible. Also not blue\n (the browser's own ::selection is blue — a highlight must not read as a\n selection), not red (error) and not the skill/agent pill hues (blue/violet).\n OPAQUE, so it composites predictably over whichever field it lands on.\n Override via --kai-color-highlight. Light: near-black text over it is 15.6:1;\n dark: near-white text over it is 6.7:1 — both well clear of WCAG AA. */\n --color-highlight: var(--kai-color-highlight, hsl(45 96% 78%));\n /* SELECTED text — the kit's own ::selection fill, applied inside every kai-*\n shadow root (see src/elements/styles.css for the scoping rationale). The kit\n owns this because leaving it to the UA meant the browser's blue outranked the\n kit's own deliberate emphasis: a marked keyword was LESS prominent than an\n incidental drag-select. Stays in the cool/blue family the UA trained everyone\n to read as \"selected\", so it can never be confused with the warm --color-highlight\n mark above; OPAQUE, so a selection over a mark composites cleanly (selection\n paints on top of a custom highlight) instead of muddying into a third colour.\n The FOREGROUND is set too, and that is not cosmetic: without it the selected\n glyph keeps its own colour, and the kit's coloured text — muted text (3.8:1),\n the skill/agent pill hues (3.6:1 / 4.0:1), inline code (4.7:1) — lands at or\n under WCAG AA on this fill. Forcing the foreground token makes the guarantee\n hold for every glyph regardless of what colour it had. Light 13.99:1,\n dark 8.87:1. Override via --kai-color-selection(-foreground). */\n --color-selection: var(--kai-color-selection, hsl(213 94% 87%));\n --color-selection-foreground: var(--kai-color-selection-foreground, hsl(240 10% 3.9%));\n --color-border: var(--kai-color-border, hsl(240 5.9% 90%));\n /* CONTROL boundary — the visible edge of an interactive form control (text\n input, textarea, select, the radio/checkbox ring). WCAG 2.1 SC 1.4.11\n Non-text Contrast requires >=3:1 against the adjacent surface for exactly\n this: a control whose boundary you cannot see is not perceivable. That is\n why this is NOT --color-border: a divider or a card outline is decorative\n and may sit below 3:1, a control's edge may not. Do NOT use border-input\n for list wrappers or panel outlines — use border-border there.\n Contrast is the requirement; a thicker border does not satisfy 1.4.11. */\n --color-input: var(--kai-color-input, hsl(240 5% 53%));\n /* Focus ring — a deliberate blue (not a neutral) so keyboard focus is always\n obvious and on-brand in both modes. Light uses a strong blue; dark uses a\n brighter one for contrast on dark surfaces. Both clear WCAG 2.1 non-text\n contrast (≥3:1) against the kit's backgrounds. Override via --kai-color-ring. */\n --color-ring: var(--kai-color-ring, hsl(217 91% 53%));\n /* Custom scrollbars — thin, rounded, subtle; thumb strengthens on hover. A\n consistent cross-platform look (the kit styles its OWN scroll regions inside\n the shadow root; the track stays transparent so nothing is forced where no\n scrollbar is needed). Override via --kai-color-scrollbar-thumb(-hover). */\n --color-scrollbar-thumb: var(--kai-color-scrollbar-thumb, hsl(240 5% 80%));\n --color-scrollbar-thumb-hover: var(--kai-color-scrollbar-thumb-hover, hsl(240 4% 64%));\n --color-sidebar: var(--kai-color-sidebar, hsl(0 0% 100%));\n\n /* Inline `code` accent. Blue text on a translucent blue chip. */\n --color-code-foreground: var(--kai-color-code-foreground, hsl(224.3 76.3% 48%));\n\n /* Tool/status chip hues. Each chip is hue text over a 15% translucent fill of\n the SAME hue (set in tool.tsx). The bare hue is too light to reach WCAG AA\n (4.5:1) on the faint fill in LIGHT mode, so the light values are darkened;\n dark mode keeps brighter hues for AA on the dark fill. Override via\n --kai-color-tool-* to retheme. */\n --color-tool-blue: var(--kai-color-tool-blue, hsl(217 91% 38%));\n --color-tool-amber: var(--kai-color-tool-amber, hsl(38 92% 28%));\n --color-tool-green: var(--kai-color-tool-green, hsl(142 71% 26%));\n --color-tool-red: var(--kai-color-tool-red, hsl(0 72% 42%));\n\n --radius: var(--kai-radius, 0.6rem);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n\n /* Typography scale — ONE scale for the whole kit. Each semantic name\n (text-micro / text-caption / text-meta / text-compact / text-body /\n text-title / text-lg)\n resolves through a namespaced --kai-text-* token, so overriding that token\n on :root restyles the kit globally — it pierces the Shadow DOM via the\n var() fallback, exactly like the --kai-color-* tokens. The bare --text-*\n names stay internal so a host's own --text-* can't collide.\n\n Tailwind's OWN scale is re-pointed at the same tokens: in Tailwind v4 the\n `text-xs` / `text-sm` / `text-base` / `text-lg` utilities are generated FROM\n --text-xs/sm/base/lg in this very @theme block, so aliasing them makes\n text-xs ≡ text-meta, text-sm ≡ text-body, text-base ≡ text-title and\n text-lg ≡ text-lg. That matters because the vast majority of the kit's call\n sites spell the Tailwind name; without the alias the --kai-text-* knobs\n reached ~12% of them and the \"globally\" promise in the theming guide was\n false. Defaults are byte-identical to stock Tailwind, so nothing moves\n until a consumer actually sets a --kai-text-* token — including in a\n consumer's own Tailwind build that imports this file, which is the point:\n one scale everywhere, kit and app moving together.\n\n Line-heights are UNITLESS RATIOS (Tailwind's own), never fixed rem. A fixed\n leading does not scale: --kai-text-body: 1.25rem under the old 1.45rem\n leading gave 20px text on 23.2px leading — near solid-set — which made the\n themable token unsafe to actually theme.\n (Message/markdown reading size also scales with the `proseSize` prop; these\n tokens cover the fixed chrome & controls.) */\n --text-micro: var(--kai-text-micro, 0.625rem); --text-micro--line-height: calc(0.875 / 0.625); /* 10px — badges, pills, uppercase eyebrows */\n --text-caption: var(--kai-text-caption, 0.6875rem); --text-caption--line-height: calc(1 / 0.6875); /* 11px — micro labels, sub-counts, xs code */\n --text-meta: var(--kai-text-meta, 0.75rem); --text-meta--line-height: calc(1 / 0.75); /* 12px — controls, toggles, switchers, captions */\n --text-xs: var(--kai-text-meta, 0.75rem); --text-xs--line-height: calc(1 / 0.75); /* ≡ text-meta */\n --text-compact: var(--kai-text-compact, 0.8125rem); --text-compact--line-height: calc(1.21875 / 0.8125); /* 13px — dense chrome & code: a notch below body */\n --text-body: var(--kai-text-body, 0.875rem); --text-body--line-height: calc(1.25 / 0.875); /* 14px — primary reading text */\n --text-sm: var(--kai-text-body, 0.875rem); --text-sm--line-height: calc(1.25 / 0.875); /* ≡ text-body */\n --text-title: var(--kai-text-title, 1rem); --text-title--line-height: calc(1.5 / 1); /* 16px — emphasis / headers */\n --text-base: var(--kai-text-title, 1rem); --text-base--line-height: calc(1.5 / 1); /* ≡ text-title */\n --text-lg: var(--kai-text-lg, 1.125rem); --text-lg--line-height: calc(1.75 / 1.125); /* 18px — section headings, lg prose */\n\n @keyframes typing { 0%,100% { transform: translateY(0); opacity: .5 } 50% { transform: translateY(-2px); opacity: 1 } }\n @keyframes loading-dots { 0%,100% { opacity: 0 } 50% { opacity: 1 } }\n @keyframes wave { 0%,100% { transform: scaleY(1) } 50% { transform: scaleY(.6) } }\n @keyframes blink { 0%,100% { opacity: 1 } 50% { opacity: 0 } }\n @keyframes text-blink { 0%,100% { color: var(--color-primary) } 50% { color: var(--color-muted-foreground) } }\n @keyframes bounce-dots { 0%,100% { transform: scale(.8); opacity: .5 } 50% { transform: scale(1.2); opacity: 1 } }\n @keyframes thin-pulse { 0%,100% { transform: scale(.95); opacity: .8 } 50% { transform: scale(1.05); opacity: .4 } }\n @keyframes pulse-dot { 0%,100% { transform: scale(1); opacity: .8 } 50% { transform: scale(1.5); opacity: 1 } }\n @keyframes shimmer-text { 0% { background-position: 150% center } 100% { background-position: -150% center } }\n @keyframes wave-bars { 0%,100% { transform: scaleY(1); opacity: .5 } 50% { transform: scaleY(.6); opacity: 1 } }\n @keyframes shimmer { 0% { background-position: 200% 50% } 100% { background-position: -200% 50% } }\n @keyframes spinner-fade { 0% { opacity: 0 } 100% { opacity: 1 } }\n @keyframes collapsible-down { from { height: 0; opacity: 0 } to { height: var(--kb-collapsible-content-height); opacity: 1 } }\n @keyframes collapsible-up { from { height: var(--kb-collapsible-content-height); opacity: 1 } to { height: 0; opacity: 0 } }\n}\n\n.dark {\n --color-background: var(--kai-color-background, hsl(50 2% 9%));\n --color-foreground: var(--kai-color-foreground, hsl(0 0% 98%));\n --color-card: var(--kai-color-card, hsl(45 4% 12%));\n --color-card-foreground: var(--kai-color-card-foreground, hsl(0 0% 98%));\n --color-popover: var(--kai-color-popover, hsl(45 4% 12%));\n --color-popover-foreground: var(--kai-color-popover-foreground, hsl(0 0% 98%));\n --color-primary: var(--kai-color-primary, hsl(0 0% 98%));\n --color-primary-foreground: var(--kai-color-primary-foreground, hsl(45 4% 11%));\n --color-secondary: var(--kai-color-secondary, hsl(45 4% 17%));\n --color-secondary-foreground: var(--kai-color-secondary-foreground, hsl(0 0% 98%));\n --color-muted: var(--kai-color-muted, hsl(45 4% 17%));\n --color-muted-foreground: var(--kai-color-muted-foreground, hsl(45 4% 64%));\n /* Re-declared in the dark scope so the color-mix recomputes with the DARK\n muted/background (a derived token is computed where it's declared, not where\n it's used — see the light --color-surface for the full note). */\n --color-surface: var(--kai-color-surface, color-mix(in srgb, var(--color-muted) 40%, var(--color-background)));\n --color-surface-strong: var(--kai-color-surface-strong, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)));\n /* Sunken: in dark a LOWER muted% is darker (blends toward the dark background). */\n --color-surface-sunken: var(--kai-color-surface-sunken, color-mix(in srgb, var(--color-muted) 18%, var(--color-background)));\n --color-accent: var(--kai-color-accent, hsl(45 4% 17%));\n --color-accent-foreground: var(--kai-color-accent-foreground, hsl(0 0% 98%));\n --color-destructive: var(--kai-color-destructive, hsl(0 62.8% 30.6%));\n --color-destructive-foreground: var(--kai-color-destructive-foreground, hsl(0 0% 98%));\n /* Semantic status colors - dark mode flips to the brighter tool-* dark hues\n (matching the chip values) carried over a near-black foreground, so the color\n both reads as text on a dark soft tint AND backs a legible foreground. See\n the light @theme note. */\n --color-success: var(--kai-color-success, hsl(142 71% 45%));\n --color-success-foreground: var(--kai-color-success-foreground, hsl(45 4% 11%));\n --color-warning: var(--kai-color-warning, hsl(38 92% 50%));\n --color-warning-foreground: var(--kai-color-warning-foreground, hsl(45 4% 11%));\n --color-info: var(--kai-color-info, hsl(217 91% 70%));\n --color-info-foreground: var(--kai-color-info-foreground, hsl(45 4% 11%));\n /* Soft status surfaces + interaction states - re-declared so each color-mix\n recomputes with the DARK hues/background (a derived token resolves where it's\n declared; see the light block for the full note). */\n --color-success-soft: var(--kai-color-success-soft, color-mix(in oklab, var(--color-success) 14%, var(--color-background)));\n --color-warning-soft: var(--kai-color-warning-soft, color-mix(in oklab, var(--color-warning) 14%, var(--color-background)));\n --color-info-soft: var(--kai-color-info-soft, color-mix(in oklab, var(--color-info) 14%, var(--color-background)));\n --color-destructive-soft: var(--kai-color-destructive-soft, color-mix(in oklab, var(--color-destructive) 14%, var(--color-background)));\n --color-hover: var(--kai-color-hover, color-mix(in oklab, var(--color-muted) 60%, var(--color-background)));\n --color-selected: var(--kai-color-selected, color-mix(in oklab, var(--color-primary) 8%, var(--color-background)));\n /* Marked text — dark. A deep warm amber rather than the light yellow: a pale\n highlighter on a near-black field would blow out and force the text to flip\n colour. See the light block for the full note. */\n --color-highlight: var(--kai-color-highlight, hsl(40 75% 26%));\n /* Selected text — dark. A deep blue rather than the light one, for the same\n reason the mark darkens. See the light block for the full note. */\n --color-selection: var(--kai-color-selection, hsl(217 60% 32%));\n --color-selection-foreground: var(--kai-color-selection-foreground, hsl(0 0% 98%));\n --color-border: var(--kai-color-border, hsl(45 4% 17%));\n /* Control boundary — see the light block. Lifted to clear 3:1 (SC 1.4.11)\n against the dark background and card surfaces. */\n --color-input: var(--kai-color-input, hsl(45 4% 48%));\n --color-ring: var(--kai-color-ring, hsl(217 91% 68%));\n --color-scrollbar-thumb: var(--kai-color-scrollbar-thumb, hsl(45 3% 30%));\n --color-scrollbar-thumb-hover: var(--kai-color-scrollbar-thumb-hover, hsl(45 3% 42%));\n --color-sidebar: var(--kai-color-sidebar, hsl(50 2% 7%));\n --color-code-foreground: var(--kai-color-code-foreground, hsl(213 94% 78%));\n\n /* Tool/status chip hues — dark mode. Brighter hues reach AA on the dark fill. */\n --color-tool-blue: var(--kai-color-tool-blue, hsl(217 91% 70%));\n --color-tool-amber: var(--kai-color-tool-amber, hsl(38 92% 50%));\n --color-tool-green: var(--kai-color-tool-green, hsl(142 71% 45%));\n --color-tool-red: var(--kai-color-tool-red, hsl(0 84% 70%));\n}\n\n/* Self-contained markdown styling — replaces the typography plugin's `prose`.\n Keyed on --color-* tokens so it themes with the rest of the kit. */\n.chat-markdown { color: var(--color-foreground); line-height: 1.6; }\n.chat-markdown > div:first-child > :first-child { margin-top: 0; }\n.chat-markdown > div:last-child > :last-child { margin-bottom: 0; }\n.chat-markdown p { margin: 0 0 0.75em; }\n.chat-markdown p:last-child { margin-bottom: 0; }\n.chat-markdown h1, .chat-markdown h2, .chat-markdown h3, .chat-markdown h4 {\n margin: 1.2em 0 0.5em; font-weight: 600; line-height: 1.3; color: var(--color-foreground);\n}\n.chat-markdown h1 { font-size: 1.4em; } .chat-markdown h2 { font-size: 1.25em; }\n.chat-markdown h3 { font-size: 1.1em; } .chat-markdown h4 { font-size: 1em; }\n.chat-markdown ul, .chat-markdown ol { margin: 0 0 0.75em; padding-left: 1.4em; }\n.chat-markdown li { margin: 0.2em 0; }\n.chat-markdown ul { list-style: disc; } .chat-markdown ol { list-style: decimal; }\n.chat-markdown a { color: var(--color-primary); text-decoration: underline; text-underline-offset: 2px; }\n.chat-markdown strong { font-weight: 600; color: var(--color-foreground); }\n.chat-markdown em { font-style: italic; }\n.chat-markdown blockquote {\n margin: 0 0 0.75em; padding-left: 0.9em; border-left: 2px solid var(--color-border);\n color: var(--color-muted-foreground);\n}\n.chat-markdown hr { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border); }\n.chat-markdown code:not(pre code) {\n background: color-mix(in oklab, var(--color-code-foreground) 15%, transparent);\n color: var(--color-code-foreground);\n border-radius: 4px; padding: 0.1em 0.35em; font-size: 0.875em;\n font-family: var(--kai-font-code, ui-monospace, \"SF Mono\", Menlo, monospace);\n}\n.chat-markdown table { width: 100%; border-collapse: collapse; margin: 0 0 0.75em; font-size: 0.9em; }\n.chat-markdown th, .chat-markdown td { border: 1px solid var(--color-border); padding: 0.4em 0.6em; text-align: left; }\n.chat-markdown th { font-weight: 600; background: color-mix(in oklab, var(--color-muted-foreground) 8%, transparent); }\n\n/* Themeable elevation. The kit is mostly flat (borders), but a few surfaces lift\n off the page — popovers, dropdowns, hover cards, cards. The shadow COLOR is a\n token (--kai-shadow-color, default near-black); set it transparent for a fully\n flat look, or tint it to match a theme. Defined here (not as a Tailwind shadow\n utility) so the color stays overridable and works in both the web-component and\n SolidJS layers. */\n.kai-elevation {\n box-shadow:\n 0 4px 12px -2px color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 14%, transparent),\n 0 2px 6px -2px color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 10%, transparent);\n}\n.kai-elevation-sm {\n box-shadow: 0 1px 2px 0 color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 8%, transparent);\n}\n\n/* Cross-platform thin scrollbar utility (Firefox + WebKit). Used by ScrollArea\n and any scroll region in the SolidJS-component build (web components also get\n this globally inside their shadow root — see src/elements/styles.css). */\n.scrollbar-thin {\n scrollbar-width: thin;\n scrollbar-color: var(--color-scrollbar-thumb) transparent;\n}\n.scrollbar-thin::-webkit-scrollbar { width: 8px; height: 8px; }\n.scrollbar-thin::-webkit-scrollbar-track { background: transparent; }\n.scrollbar-thin::-webkit-scrollbar-thumb {\n background-color: var(--color-scrollbar-thumb);\n border-radius: 9999px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n.scrollbar-thin::-webkit-scrollbar-thumb:hover { background-color: var(--color-scrollbar-thumb-hover); }\n.scrollbar-thin::-webkit-scrollbar-corner { background: transparent; }\n\n/* Surface utilities — `bg-surface` / `bg-surface-strong` paint the opaque,\n theme-derived surface tokens (see --color-surface above). Defined explicitly\n because a color-mix-valued @theme token does NOT emit a bare `bg-` utility on\n its own (only opacity variants). Use these for panels that sit OVER content\n instead of `bg-muted/NN`. */\n@utility bg-surface {\n background-color: var(--color-surface);\n}\n@utility bg-surface-strong {\n background-color: var(--color-surface-strong);\n}\n@utility bg-surface-sunken {\n background-color: var(--color-surface-sunken);\n}\n\n/* Soft-status + interaction-state utilities. Same reason as bg-surface above: a\n color-mix-valued @theme token does NOT emit a bare `bg-` utility on its own, so\n bg-success-soft / bg-warning-soft / bg-info-soft / bg-destructive-soft and\n bg-hover / bg-selected are declared here. (The SOLID tokens - success / warning\n / info + their -foreground - are plain hsl values, so bg-success / text-success\n etc. ARE auto-generated by @theme and need no explicit utility.) */\n@utility bg-success-soft {\n background-color: var(--color-success-soft);\n}\n@utility bg-warning-soft {\n background-color: var(--color-warning-soft);\n}\n@utility bg-info-soft {\n background-color: var(--color-info-soft);\n}\n@utility bg-destructive-soft {\n background-color: var(--color-destructive-soft);\n}\n@utility bg-hover {\n background-color: var(--color-hover);\n}\n@utility bg-selected {\n background-color: var(--color-selected);\n}\n";
|
|
8209
9001
|
function declaredKaiTokens(css) {
|
|
8210
9002
|
const names = /* @__PURE__ */ new Set();
|
|
8211
9003
|
for (const m of css.matchAll(/var\(\s*(--kai-[a-zA-Z0-9-]+)\s*[,)]/g)) names.add(m[1]);
|
|
@@ -8620,11 +9412,54 @@ const RULES = [
|
|
|
8620
9412
|
title: "`kai-compare` — two candidates, `data` as a JS property, terminal pick",
|
|
8621
9413
|
cause: "`<kai-compare>` shows EXACTLY two assistant candidates for one prompt and lets the user pick the better one. The `data` value is an array/object, so it must be set as a JS PROPERTY (never an HTML attribute). Both candidates can stream — but, like `kai-chat`, that needs a NEW `data` reference per chunk (mutating in place will not re-render). The pick is a COMMIT (not a Submit): it fires once and the card collapses.",
|
|
8622
9414
|
fix: "Set `data` in JS with two candidates, stream by reassigning a fresh `data` object per chunk, and listen for `kai-compare-select` directly on the element.\n\n```ts\nimport { toast } from '@kitn.ai/ui/elements';\nimport type { ResponseCompareData, CompareSelection } from '@kitn.ai/ui';\n\nconst el = document.querySelector('kai-compare')!;\n// data is a JS PROPERTY — exactly two candidates, each with a unique id.\nel.data = {\n prompt: 'Summarise the report',\n candidates: [\n { id: 'a', content: '', streaming: true },\n { id: 'b', content: '', streaming: true },\n ],\n} satisfies ResponseCompareData;\n\n// Stream BOTH columns: replace data with a NEW object per chunk.\nel.data = { ...el.data, candidates: [{ ...a, content: aText }, { ...b, content: bText }] };\n// Clear `streaming` on a candidate when it settles — the pick stays disabled\n// until BOTH have settled, then `kai-ready` fires.\n\n// Picking is terminal: emits { chosenId, rejectedIds, at } and collapses.\nel.addEventListener('kai-compare-select', (e) => {\n const { chosenId, rejectedIds } = (e as CustomEvent<CompareSelection>).detail;\n recordPreference({ prompt, chosen: chosenId, rejected: rejectedIds });\n});\n```\n\nA malformed definition (not two candidates, missing/duplicate ids) fires `kai-error` instead. The event is non-bubbling — listen on the element, not on `document`."
|
|
9415
|
+
},
|
|
9416
|
+
{
|
|
9417
|
+
// Rule 13 — kai-composer is the bare editor; attachments + send live on kai-prompt-input
|
|
9418
|
+
// Source: src/elements/composer.tsx (element doc), src/elements/prompt-input.tsx,
|
|
9419
|
+
// apps/docs components/composer.mdx (the taxonomy sentence). Rung-6 F-43.
|
|
9420
|
+
id: "composer-attachments",
|
|
9421
|
+
test: (t) => {
|
|
9422
|
+
if (!/kai-composer|composer\b/i.test(t)) return false;
|
|
9423
|
+
return /attach|paperclip|file.?upload|upload|send.?button|toolbar|staging|staged/i.test(t);
|
|
9424
|
+
},
|
|
9425
|
+
title: "`<kai-composer>` is the bare editor — attachments and the send button live on `<kai-prompt-input>`",
|
|
9426
|
+
cause: "`<kai-composer>` is deliberately the bare editing surface: rich text, entity pills, triggers, Enter-to-submit — and nothing else. No send button, no toolbar, no attachment surface; its `kai-submit` detail is `{ doc, text, entities }` with no `attachments` field. Nothing is missing — the batteries-included wrapper is a different element.",
|
|
9427
|
+
fix: "Reach for `<kai-prompt-input>`, which is built on the composer and adds the send button, the toolbar, and attachments: a paperclip that stages files as chips, a `kai-attachments-change` event, and `attachments` on its `kai-submit` detail.\n\n```html\n<kai-prompt-input placeholder=\"Send a message...\"></kai-prompt-input>\n```\n\n```js\ndocument.querySelector('kai-prompt-input').addEventListener('kai-submit', (e) => {\n const { value, attachments } = e.detail; // attachments staged via the paperclip\n});\n```\n\nKeep `<kai-composer>` only when you are composing the input row yourself — then the picker, staging tray and send control are yours to build, and every staged file’s `url` must be a `data:` URI, never `URL.createObjectURL` (see the attachment-blob-url rule)."
|
|
9428
|
+
},
|
|
9429
|
+
{
|
|
9430
|
+
// Rule 14 — blob: attachment URLs: the wire refuses them; use a data: URI
|
|
9431
|
+
// Source: src/components/attachment-types.ts (AttachmentData.url doc),
|
|
9432
|
+
// src/elements/default-input.tsx (readAsDataUrl), src/wire/files.ts (the refusal),
|
|
9433
|
+
// apps/docs patterns/attachments-flow.mdx. Rung-6 F-44; the defect PR #186 shipped.
|
|
9434
|
+
id: "attachment-blob-url",
|
|
9435
|
+
test: (t) => {
|
|
9436
|
+
if (/createObjectURL/.test(t) && /attachment|type:\s*'file'|filename|kai|@kitn/i.test(t)) return true;
|
|
9437
|
+
if (/blob:/.test(t) && /attachment|toOpenAIMessages|toAnthropicMessages|wire|kai|@kitn/i.test(t)) return true;
|
|
9438
|
+
return false;
|
|
9439
|
+
},
|
|
9440
|
+
title: "Never `URL.createObjectURL` in `AttachmentData.url` — read the file as a `data:` URI",
|
|
9441
|
+
cause: "An object URL resolves only inside the browser tab that minted it, so `url: URL.createObjectURL(file)` renders a flawless local preview and is meaningless to any provider. `toOpenAIMessages` / `toAnthropicMessages` REFUSE a `blob:` URL rather than send an address the model cannot fetch — before they refused it, an attachment-only turn reached the model as nothing at all. A mock-only app hides the defect (`createMockResponder` never encodes the thread back); it surfaces the moment you point at a real provider.",
|
|
9442
|
+
fix: "Read every staged file as a `data:` URI and put THAT in `url` — it previews identically and is the one form both provider wires actually take:\n\n```js\nconst readAsDataUrl = (file) =>\n new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(reader.error);\n reader.readAsDataURL(file);\n });\n\nconst url = await readAsDataUrl(file); // data: URI — never URL.createObjectURL\nreturn { id, type: 'file', filename: file.name, mediaType: file.type, url };\n```\n\n`<kai-prompt-input>`’s built-in paperclip already does this for you; the conversion is only yours when you stage files yourself. With `data:` URIs there is also nothing to revoke — no object-URL lifecycle to manage."
|
|
9443
|
+
},
|
|
9444
|
+
{
|
|
9445
|
+
// Rule 15 — mock tool calls: MockTurn.toolCalls scripts them; the HOST resolves them
|
|
9446
|
+
// Source: src/state/mock.ts (MockTurn/MockToolCall), src/wire/chunk.ts (ModelTurn.toolCalls),
|
|
9447
|
+
// src/state/stream.ts (upsertTool). Rung-6 F-46/F-47/F-52.
|
|
9448
|
+
id: "mock-tool-calls",
|
|
9449
|
+
test: (t) => {
|
|
9450
|
+
const toolContext = /tool.?call|toolCalls|input-available/i.test(t);
|
|
9451
|
+
if (/createMockResponder|mock responder|MockTurn/i.test(t) && toolContext) return true;
|
|
9452
|
+
if (toolContext && /\bmock\b/i.test(t) && /kai|@kitn|stream|responder/i.test(t)) return true;
|
|
9453
|
+
return false;
|
|
9454
|
+
},
|
|
9455
|
+
title: "Scripting mock tool calls — `MockTurn.toolCalls`; the HOST resolves the announced call",
|
|
9456
|
+
cause: "Two separate facts. (1) The default mock replies are text-only, so out of the box `createMockResponder()` never announces a tool call — scripting one takes a `MockTurn` reply. (2) A tool call — mock or real provider — is only ever ANNOUNCED by the stream: the part parks at `state: 'input-available'` and stays there. The kit parses and renders the call; EXECUTING it and answering is the host’s job.",
|
|
9457
|
+
fix: "Script the call with a `MockTurn` reply (`{ text?, toolCalls? }`); the mock frames it announce-then-arguments with `finish_reason: 'tool_calls'`, exactly as a real OpenAI-wire turn, so it flows through `readOpenAIStream` unchanged:\n\n```js\nimport { createMockResponder } from '@kitn.ai/ui/state';\n\nconst mock = createMockResponder({\n replies: [\n 'Plain text turn.',\n { text: 'Checking…', toolCalls: [{ name: 'get_weather', arguments: { city: 'Oslo' } }] },\n ],\n});\n```\n\nThen resolve each announced call yourself after the read settles — walk the returned `ModelTurn.toolCalls` and patch the part to `output-available`:\n\n```js\nconst result = await readOpenAIStream(response, stream);\nfor (const call of result.toolCalls) {\n const output = await runTool(call.name, call.input); // your side of the seam\n stream.upsertTool(call.id, { state: 'output-available', output });\n}\n```\n\nSkip any call with `providerExecuted: true` — the provider already ran it and its result arrived in-stream."
|
|
8623
9458
|
}
|
|
8624
9459
|
];
|
|
8625
9460
|
function buildText(matched) {
|
|
8626
9461
|
if (matched.length === 0) {
|
|
8627
|
-
return
|
|
9462
|
+
return 'No known failure pattern matched. Suggested next steps:\n\n1. Use the `component_reference` tool to look up the real API for the element (prop names, event names, attribute vs. property distinction).\n2. If the problem is in host code rather than an element — streaming, `createAssistantStream`, `createMockResponder`, the SSE readers, the encoders — call `component_reference` with `{ name: "programmatic" }` for the full `@kitn.ai/ui/state` + `@kitn.ai/ui/wire` API, and `{ name: "composed-thread" }` for a complete hand-composed surface to compare wiring against.\n3. Check the **Streaming recipe** and the **Programmatic layer** sections in `llms-full.txt` (`node_modules/@kitn.ai/ui/llms-full.txt` or https://ui.kitn.ai/llms-full.txt) for correct streaming wiring.\n4. Paste `https://ui.kitn.ai/llms.txt` into your prompt for a compact orientation.';
|
|
8628
9463
|
}
|
|
8629
9464
|
const sections = matched.map((rule, i) => {
|
|
8630
9465
|
const n = i + 1;
|
|
@@ -8654,6 +9489,377 @@ const debug = {
|
|
|
8654
9489
|
};
|
|
8655
9490
|
}
|
|
8656
9491
|
};
|
|
9492
|
+
const SAFE_SCHEMES = ["http:", "https:", "mailto:"];
|
|
9493
|
+
function schemeOf(url) {
|
|
9494
|
+
try {
|
|
9495
|
+
return new URL(url, "http://_invalid_base").protocol;
|
|
9496
|
+
} catch {
|
|
9497
|
+
return void 0;
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
9500
|
+
function isSafeUrl(url) {
|
|
9501
|
+
const scheme = schemeOf(url);
|
|
9502
|
+
return scheme !== void 0 && SAFE_SCHEMES.includes(scheme);
|
|
9503
|
+
}
|
|
9504
|
+
const CONSTRUCT_SCHEMA_URL = "https://ui.kitn.ai/schemas/construct/v1.json";
|
|
9505
|
+
const TAG_RE = /^[a-z][a-z0-9]*-[a-z0-9-]+$/;
|
|
9506
|
+
const ProviderSchema = z$1.discriminatedUnion("mode", [
|
|
9507
|
+
z$1.object({ mode: z$1.literal("mock") }).strict(),
|
|
9508
|
+
z$1.object({
|
|
9509
|
+
mode: z$1.literal("endpoint"),
|
|
9510
|
+
/** The CONSUMER's chat route. Kit parses, consumer fetches. */
|
|
9511
|
+
url: z$1.string().min(1),
|
|
9512
|
+
wire: z$1.enum(["openai", "anthropic"])
|
|
9513
|
+
}).strict()
|
|
9514
|
+
]);
|
|
9515
|
+
const ConstructSchema = z$1.object({
|
|
9516
|
+
$schema: z$1.string().optional(),
|
|
9517
|
+
/** The emitted tag: <acme-support>. Must satisfy customElements.define. */
|
|
9518
|
+
name: z$1.string().regex(TAG_RE, 'must be a valid custom-element tag: lowercase, with a hyphen (e.g. "acme-support")'),
|
|
9519
|
+
// Widened progressively: fullscreen/aside/split landed in Task 12, custom in Task 13.
|
|
9520
|
+
layout: z$1.enum(["widget", "fullscreen", "aside", "split", "custom"]),
|
|
9521
|
+
provider: ProviderSchema,
|
|
9522
|
+
/** Plain (unsigned) identity passthrough — no signing/auth infra (owner
|
|
9523
|
+
* ruling, 2026-08-26: signed JWT/HMAC identity is later additive
|
|
9524
|
+
* vocabulary, not this field). TOP-LEVEL, not nested under `provider`:
|
|
9525
|
+
* local history scoping (`capabilities.history.persistence: 'local'`)
|
|
9526
|
+
* needs userId independent of provider mode — nesting it inside the
|
|
9527
|
+
* provider union would make it silently inert for `mode: 'mock'` +
|
|
9528
|
+
* local history, a real and common combination. Threaded as the
|
|
9529
|
+
* `x-kai-user-id` header on every emitted fetch to the CONSUMER's own
|
|
9530
|
+
* backend (the endpoint provider's chat POST, history's endpoint
|
|
9531
|
+
* GET/PUT), and folded into the localStorage key for `local` history so
|
|
9532
|
+
* different users on the same browser profile don't share one thread.
|
|
9533
|
+
* Constrained to printable-ASCII (no CR/LF, no non-ISO-8859-1 code
|
|
9534
|
+
* points) beyond the usual construct-authored-text escaping: this value
|
|
9535
|
+
* reaches an HTTP HEADER VALUE, not just a JS string literal, and
|
|
9536
|
+
* `fetch()` throws at RUNTIME on a header value containing CR/LF or
|
|
9537
|
+
* outside ISO-8859-1 — a bad userId would surface as an opaque crash in
|
|
9538
|
+
* the CONSUMER's generated app, not a construct-validation error. Reject
|
|
9539
|
+
* loudly here instead, where the author gets a path + message. */
|
|
9540
|
+
userId: z$1.string().min(1).regex(
|
|
9541
|
+
/^[\x20-\x7E]+$/,
|
|
9542
|
+
"must be printable ASCII with no line breaks (it becomes an HTTP header value)"
|
|
9543
|
+
).optional(),
|
|
9544
|
+
theme: z$1.object({
|
|
9545
|
+
/** Any CSS color; becomes --kai-color-primary on the host. */
|
|
9546
|
+
accent: z$1.string().optional(),
|
|
9547
|
+
mode: z$1.enum(["light", "dark", "system"]).default("system")
|
|
9548
|
+
}).strict().optional(),
|
|
9549
|
+
/** Construct-wide header chrome, valid on every layout (not layout-scoped
|
|
9550
|
+
* like `widget`, and not a capability toggle — a header is a construct-
|
|
9551
|
+
* wide fact, like `theme`). Rendered inside ChatThread's own built-in
|
|
9552
|
+
* header bar; a logo/icon projects through the kit's EXISTING
|
|
9553
|
+
* `header-start` named slot (`slots` vocabulary above), not a second
|
|
9554
|
+
* image-prop convention here. */
|
|
9555
|
+
header: z$1.object({
|
|
9556
|
+
/** Rendered in ChatThread's built-in header bar (left side). Construct-
|
|
9557
|
+
* authored/untrusted text, like theme.accent/provider.url — JSON.stringify'd
|
|
9558
|
+
* at its one emit site, never a raw JSX attribute string. */
|
|
9559
|
+
title: z$1.string().min(1).optional()
|
|
9560
|
+
}).strict().optional(),
|
|
9561
|
+
/** Greeting shown while the thread is empty (no messages yet) — the
|
|
9562
|
+
* proven "welcome screen" pattern (Intercom-class): title + optional
|
|
9563
|
+
* description + optional icon above, with `capabilities.starters`'
|
|
9564
|
+
* chips and the composer still rendering below (ChatThread's `empty`
|
|
9565
|
+
* REPLACE slot only stands in for the empty MESSAGE LIST — see
|
|
9566
|
+
* chat-thread.tsx's own doc comment on `empty` — so the chips are
|
|
9567
|
+
* never lost). Construct-wide like `header`, not a capability: it's a
|
|
9568
|
+
* fact about the empty state, not a toggleable affordance.
|
|
9569
|
+
* `title`/`description` are construct-authored/untrusted text, like
|
|
9570
|
+
* `header.title`/`theme.accent`/`provider.url` — JSON.stringify'd at
|
|
9571
|
+
* their one emit site, never a raw JSX attribute string. `icon` is a
|
|
9572
|
+
* URL reaching an `<img src>` sink in emitted code, exactly like
|
|
9573
|
+
* `widget.launcherIcon` — same `isSafeUrl` policy, same superRefine
|
|
9574
|
+
* shape, below. */
|
|
9575
|
+
empty: z$1.object({
|
|
9576
|
+
title: z$1.string().min(1),
|
|
9577
|
+
description: z$1.string().min(1).optional(),
|
|
9578
|
+
icon: z$1.string().min(1).optional()
|
|
9579
|
+
}).strict().optional(),
|
|
9580
|
+
// Capability vocabulary, widened one field at a time by later tasks.
|
|
9581
|
+
capabilities: z$1.object({
|
|
9582
|
+
/** Starter prompts shown on the empty thread; clicking one sends it.
|
|
9583
|
+
* 1-6 non-empty strings — construct-authored text, escaped like
|
|
9584
|
+
* `theme.accent`/`provider.url` at every emit interpolation site. */
|
|
9585
|
+
starters: z$1.array(z$1.string().min(1)).min(1).max(6).optional(),
|
|
9586
|
+
/** Enables the paperclip attach affordance; accept is a non-empty
|
|
9587
|
+
* list of media types/globs, e.g. ["image/*", "application/pdf"] —
|
|
9588
|
+
* WHETHER stays with the construct author (this field), HOW stays
|
|
9589
|
+
* with the kit (ChatThread's own attach/accept props, threaded
|
|
9590
|
+
* through by codegen). */
|
|
9591
|
+
attachments: z$1.object({
|
|
9592
|
+
/** Accept-list of media types/globs, e.g. ["image/*", "application/pdf"]. */
|
|
9593
|
+
accept: z$1.array(z$1.string().min(1)).min(1)
|
|
9594
|
+
}).strict().optional(),
|
|
9595
|
+
/** Conversation persistence. `none` (default, nothing emitted): the
|
|
9596
|
+
* thread lives only in memory for the tab's lifetime. `local`:
|
|
9597
|
+
* persisted to this browser's localStorage, keyed by the construct's
|
|
9598
|
+
* tag — a mechanism decision (WHERE); what to retain and for how
|
|
9599
|
+
* long stays an app decision (component-scope-boundary), so no
|
|
9600
|
+
* retention count/quota lands here. `endpoint`: the CONSUMER's own
|
|
9601
|
+
* thread route (GET returns ChatMessage[], PUT stores them) —
|
|
9602
|
+
* requires `url`; `url` is rejected for any other persistence
|
|
9603
|
+
* (superRefine below, both directions loud). */
|
|
9604
|
+
history: z$1.object({
|
|
9605
|
+
persistence: z$1.enum(["none", "local", "endpoint"]),
|
|
9606
|
+
/** endpoint persistence only: the CONSUMER's thread routes (GET returns
|
|
9607
|
+
* ChatMessage[], PUT stores them). Refined below. */
|
|
9608
|
+
url: z$1.string().min(1).optional()
|
|
9609
|
+
}).strict().optional(),
|
|
9610
|
+
/** How the model's thinking (reasoning parts) renders. `'full'`
|
|
9611
|
+
* (the default when omitted — see codegen.ts's emitReasoningProp)
|
|
9612
|
+
* is the collapsible "Thinking" disclosure, shimmering while it
|
|
9613
|
+
* streams. `'compact'` shows only a shimmer/typing loader while
|
|
9614
|
+
* reasoning streams, with no expandable detail. `'off'` hides
|
|
9615
|
+
* reasoning entirely. This is HOW an existing medium fact (the
|
|
9616
|
+
* model's thinking) displays — the kit's call — so it maps straight
|
|
9617
|
+
* onto ChatThread's own `reasoning` prop; there is no app-layer
|
|
9618
|
+
* quota or retention decision hiding in it. Left `.optional()`
|
|
9619
|
+
* rather than `.default('full')`, matching every sibling field in
|
|
9620
|
+
* this object: a zod `.default()` here would make `reasoning`
|
|
9621
|
+
* REQUIRED on the inferred input type (z.infer is the output type),
|
|
9622
|
+
* breaking every `capabilities: {...}` object literal in this file
|
|
9623
|
+
* and its tests that doesn't mention it. */
|
|
9624
|
+
reasoning: z$1.enum(["full", "compact", "off"]).optional(),
|
|
9625
|
+
/** Seeds the reasoning disclosure open AND keeps it tracking the
|
|
9626
|
+
* stream (open while streaming, closes when it settles) — the
|
|
9627
|
+
* pre-Task-19f `full` behavior. Default false/absent: the panel
|
|
9628
|
+
* starts closed (just the "Thinking" shimmer chip) and only opens
|
|
9629
|
+
* on click — the current default (owner ruling, 2026-08-26).
|
|
9630
|
+
* Meaningless when `reasoning` is `'compact'` (no expandable
|
|
9631
|
+
* content exists) or `'off'` (nothing renders); rejected on both,
|
|
9632
|
+
* loudly, below. */
|
|
9633
|
+
reasoningOpen: z$1.boolean().optional()
|
|
9634
|
+
}).strict().optional(),
|
|
9635
|
+
/** Named generative-UI card definitions the model can emit as tool calls,
|
|
9636
|
+
* rendered in the thread. Top-level (not a capability): a card is
|
|
9637
|
+
* something the construct's model can DO at any point once a card is
|
|
9638
|
+
* declared, not a thread-affordance toggle like starters/attachments/
|
|
9639
|
+
* reasoning. Reuses the kit's own card-tool projection end to end
|
|
9640
|
+
* (`cardTools`/`toOpenAITools`/`toAnthropicTools`, `@kitn.ai/ui/schemas`)
|
|
9641
|
+
* — never a second one authored here. `schema` is validated structurally
|
|
9642
|
+
* only; deep card validation (incl. `x-kai-format` mask hints) is the
|
|
9643
|
+
* kit's own card contract at render time. */
|
|
9644
|
+
cards: z$1.array(
|
|
9645
|
+
z$1.object({
|
|
9646
|
+
/** Tool-facing card name, e.g. "refund_approval". */
|
|
9647
|
+
name: z$1.string().regex(/^[a-z][a-z0-9_]*$/),
|
|
9648
|
+
/** The kit's card schema JSON (incl. x-kai-format mask hints).
|
|
9649
|
+
* Validated structurally here; deep card validation is the kit's
|
|
9650
|
+
* own card contract at render time. */
|
|
9651
|
+
schema: z$1.record(z$1.string(), z$1.unknown())
|
|
9652
|
+
}).strict()
|
|
9653
|
+
).min(1).optional(),
|
|
9654
|
+
/** Named `<slot>` projection points the emitted web component exposes —
|
|
9655
|
+
* the format's ONLY escape hatch (named slots, no code-in-JSON). Each
|
|
9656
|
+
* name must be a valid HTML slot-attribute value AND a legible
|
|
9657
|
+
* identifier: kebab-case, starting with a letter (the same shape as a
|
|
9658
|
+
* CSS custom-ident) — `slot name="Header!"` is rejected, not sanitized,
|
|
9659
|
+
* matching the format's loud-rejection discipline everywhere else.
|
|
9660
|
+
* 1-8 entries, no duplicates (superRefine below — a regex alone can't
|
|
9661
|
+
* see across array entries). `layout: 'custom'` requires at least one
|
|
9662
|
+
* declared slot (superRefine below): `custom` IS the slots grain — a
|
|
9663
|
+
* `custom` layout with nothing to project into is not meaningfully
|
|
9664
|
+
* different from `fullscreen`. */
|
|
9665
|
+
slots: z$1.array(z$1.string().regex(/^[a-z][a-z0-9-]*$/, "slot names must be kebab-case, starting with a letter")).min(1).max(8).optional(),
|
|
9666
|
+
/** Layout-scoped FAB chrome, `layout: 'widget'` only (superRefine below).
|
|
9667
|
+
* Purely additive sibling to `layout` (widen-never-restructure) — mirrors
|
|
9668
|
+
* Dock's own props verbatim, threaded through by codegen's emitLayoutOpen. */
|
|
9669
|
+
widget: z$1.object({
|
|
9670
|
+
/** Which corner. Mirrors Dock's own DockPosition enum verbatim — not a
|
|
9671
|
+
* new left/right binary. Omitted keeps Dock's own default (bottom-end). */
|
|
9672
|
+
position: z$1.enum(["bottom-end", "bottom-start", "top-end", "top-start"]).optional(),
|
|
9673
|
+
/** An image URL for the closed-state launcher glyph, replacing Dock's
|
|
9674
|
+
* built-in chat-bubble icon. Construct-authored/untrusted text, like
|
|
9675
|
+
* theme.accent and provider.url — escaped the same way at its one emit site. */
|
|
9676
|
+
launcherIcon: z$1.string().min(1).optional(),
|
|
9677
|
+
/** Seed Dock's open state at mount. Uncontrolled — never steals focus (Dock's
|
|
9678
|
+
* own focus contract, dock.tsx). Omitted keeps Dock's own default (closed). */
|
|
9679
|
+
defaultOpen: z$1.boolean().optional()
|
|
9680
|
+
}).strict().optional()
|
|
9681
|
+
}).strict().superRefine((construct, ctx) => {
|
|
9682
|
+
if (construct.slots) {
|
|
9683
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9684
|
+
construct.slots.forEach((name, i) => {
|
|
9685
|
+
if (seen.has(name)) {
|
|
9686
|
+
ctx.addIssue({
|
|
9687
|
+
code: z$1.ZodIssueCode.custom,
|
|
9688
|
+
path: ["slots", i],
|
|
9689
|
+
message: `duplicate slot name "${name}"`
|
|
9690
|
+
});
|
|
9691
|
+
}
|
|
9692
|
+
seen.add(name);
|
|
9693
|
+
});
|
|
9694
|
+
}
|
|
9695
|
+
if (construct.layout === "custom" && (!construct.slots || construct.slots.length === 0)) {
|
|
9696
|
+
ctx.addIssue({
|
|
9697
|
+
code: z$1.ZodIssueCode.custom,
|
|
9698
|
+
path: ["slots"],
|
|
9699
|
+
message: '"custom" layout requires at least one declared slot — custom IS the slots grain'
|
|
9700
|
+
});
|
|
9701
|
+
}
|
|
9702
|
+
if (construct.layout === "split" && construct.slots) {
|
|
9703
|
+
const i = construct.slots.indexOf("pane");
|
|
9704
|
+
if (i !== -1) {
|
|
9705
|
+
ctx.addIssue({
|
|
9706
|
+
code: z$1.ZodIssueCode.custom,
|
|
9707
|
+
path: ["slots", i],
|
|
9708
|
+
message: `"pane" collides with the "split" layout's own fixed <slot name="pane"> — choose a different slot name`
|
|
9709
|
+
});
|
|
9710
|
+
}
|
|
9711
|
+
}
|
|
9712
|
+
if (construct.widget && construct.layout !== "widget") {
|
|
9713
|
+
ctx.addIssue({
|
|
9714
|
+
code: z$1.ZodIssueCode.custom,
|
|
9715
|
+
path: ["widget"],
|
|
9716
|
+
message: '"widget" is only valid on layout: "widget"'
|
|
9717
|
+
});
|
|
9718
|
+
}
|
|
9719
|
+
if (construct.widget?.launcherIcon && !isSafeUrl(construct.widget.launcherIcon)) {
|
|
9720
|
+
ctx.addIssue({
|
|
9721
|
+
code: z$1.ZodIssueCode.custom,
|
|
9722
|
+
path: ["widget", "launcherIcon"],
|
|
9723
|
+
message: "launcherIcon must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
|
|
9724
|
+
});
|
|
9725
|
+
}
|
|
9726
|
+
if (construct.empty?.icon && !isSafeUrl(construct.empty.icon)) {
|
|
9727
|
+
ctx.addIssue({
|
|
9728
|
+
code: z$1.ZodIssueCode.custom,
|
|
9729
|
+
path: ["empty", "icon"],
|
|
9730
|
+
message: "icon must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
|
|
9731
|
+
});
|
|
9732
|
+
}
|
|
9733
|
+
const reasoning = construct.capabilities?.reasoning;
|
|
9734
|
+
const reasoningOpen = construct.capabilities?.reasoningOpen;
|
|
9735
|
+
if (reasoningOpen !== void 0 && (reasoning === "compact" || reasoning === "off")) {
|
|
9736
|
+
ctx.addIssue({
|
|
9737
|
+
code: z$1.ZodIssueCode.custom,
|
|
9738
|
+
path: ["capabilities", "reasoningOpen"],
|
|
9739
|
+
message: '"reasoningOpen" only applies when reasoning is "full" or omitted — "compact"/"off" have no disclosure to open'
|
|
9740
|
+
});
|
|
9741
|
+
}
|
|
9742
|
+
const history = construct.capabilities?.history;
|
|
9743
|
+
if (!history) return;
|
|
9744
|
+
if (history.persistence === "endpoint" && !history.url) {
|
|
9745
|
+
ctx.addIssue({
|
|
9746
|
+
code: z$1.ZodIssueCode.custom,
|
|
9747
|
+
path: ["capabilities", "history", "url"],
|
|
9748
|
+
message: '"endpoint" persistence requires a url'
|
|
9749
|
+
});
|
|
9750
|
+
}
|
|
9751
|
+
if (history.persistence !== "endpoint" && history.url !== void 0) {
|
|
9752
|
+
ctx.addIssue({
|
|
9753
|
+
code: z$1.ZodIssueCode.custom,
|
|
9754
|
+
path: ["capabilities", "history", "url"],
|
|
9755
|
+
message: 'url is only valid with "endpoint" persistence'
|
|
9756
|
+
});
|
|
9757
|
+
}
|
|
9758
|
+
});
|
|
9759
|
+
function validateConstruct(input) {
|
|
9760
|
+
const parsed = ConstructSchema.safeParse(input);
|
|
9761
|
+
if (parsed.success) return { ok: true, construct: parsed.data };
|
|
9762
|
+
return {
|
|
9763
|
+
ok: false,
|
|
9764
|
+
problems: parsed.error.issues.flatMap((issue) => {
|
|
9765
|
+
if (issue.code === "unrecognized_keys") {
|
|
9766
|
+
return issue.keys.map((key) => ({
|
|
9767
|
+
path: [...issue.path.map(String), key].join("."),
|
|
9768
|
+
message: `"${key}" is not construct vocabulary`
|
|
9769
|
+
}));
|
|
9770
|
+
}
|
|
9771
|
+
return [
|
|
9772
|
+
{
|
|
9773
|
+
path: issue.path.map(String).join("."),
|
|
9774
|
+
message: issue.message
|
|
9775
|
+
}
|
|
9776
|
+
];
|
|
9777
|
+
})
|
|
9778
|
+
};
|
|
9779
|
+
}
|
|
9780
|
+
const inputSchema = z$1.object({
|
|
9781
|
+
intent: z$1.string().min(1).optional().describe("What the author wants, in their words. Alone: returns a starter construct."),
|
|
9782
|
+
construct: z$1.record(z$1.string(), z$1.unknown()).optional().describe("The FULL construct JSON for this turn. The harness owns the file; send all of it every turn.")
|
|
9783
|
+
}).strict();
|
|
9784
|
+
function starterFor(intent) {
|
|
9785
|
+
const impliesWidget = /widget|embed|bubble|corner|launcher/i.test(intent);
|
|
9786
|
+
const name = "my-chat";
|
|
9787
|
+
return {
|
|
9788
|
+
construct: {
|
|
9789
|
+
$schema: CONSTRUCT_SCHEMA_URL,
|
|
9790
|
+
name,
|
|
9791
|
+
layout: "widget",
|
|
9792
|
+
provider: { mode: "mock" }
|
|
9793
|
+
},
|
|
9794
|
+
stated: impliesWidget ? ["layout: widget (implied by your request)"] : [],
|
|
9795
|
+
questions: [
|
|
9796
|
+
`What should the element tag be? (kebab-case, e.g. "acme-support"; using "${name}" until you say)`,
|
|
9797
|
+
...impliesWidget ? [] : ["Layout: widget, fullscreen, aside, split, or custom?"]
|
|
9798
|
+
]
|
|
9799
|
+
};
|
|
9800
|
+
}
|
|
9801
|
+
const constructTool = {
|
|
9802
|
+
name: "construct",
|
|
9803
|
+
description: `Author a kitn construct (one JSON file → one web component) turn by turn. Send the full construct each turn; invalid turns come back REJECTED with per-path reasons and the previous good construct stands. Schema: ${CONSTRUCT_SCHEMA_URL}. Preview with "kai dev <file>".`,
|
|
9804
|
+
inputSchema,
|
|
9805
|
+
handler: async (args) => {
|
|
9806
|
+
const { intent, construct } = args;
|
|
9807
|
+
if (!intent && !construct) {
|
|
9808
|
+
return {
|
|
9809
|
+
isError: true,
|
|
9810
|
+
content: [{ type: "text", text: "construct: pass intent, construct, or both" }]
|
|
9811
|
+
};
|
|
9812
|
+
}
|
|
9813
|
+
if (!construct) {
|
|
9814
|
+
const s = starterFor(intent ?? "");
|
|
9815
|
+
return {
|
|
9816
|
+
content: [
|
|
9817
|
+
{
|
|
9818
|
+
type: "text",
|
|
9819
|
+
text: [
|
|
9820
|
+
"STARTER construct (mock provider — keyless, previews immediately):",
|
|
9821
|
+
"```json",
|
|
9822
|
+
JSON.stringify(s.construct, null, 2),
|
|
9823
|
+
"```",
|
|
9824
|
+
...s.stated,
|
|
9825
|
+
...s.questions
|
|
9826
|
+
].join("\n")
|
|
9827
|
+
}
|
|
9828
|
+
]
|
|
9829
|
+
};
|
|
9830
|
+
}
|
|
9831
|
+
const out = validateConstruct(construct);
|
|
9832
|
+
if (!out.ok) {
|
|
9833
|
+
return {
|
|
9834
|
+
content: [
|
|
9835
|
+
{
|
|
9836
|
+
type: "text",
|
|
9837
|
+
text: [
|
|
9838
|
+
"REJECTED — this turn does not change the file; the previous good construct stands.",
|
|
9839
|
+
...out.problems.map((p) => ` ${p.path || "(root)"}: ${p.message}`)
|
|
9840
|
+
].join("\n")
|
|
9841
|
+
}
|
|
9842
|
+
]
|
|
9843
|
+
};
|
|
9844
|
+
}
|
|
9845
|
+
const c = out.construct;
|
|
9846
|
+
return {
|
|
9847
|
+
content: [
|
|
9848
|
+
{
|
|
9849
|
+
type: "text",
|
|
9850
|
+
text: [
|
|
9851
|
+
`VALID: <${c.name}> (${c.layout}, ${c.provider.mode}).`,
|
|
9852
|
+
"```json",
|
|
9853
|
+
JSON.stringify(c, null, 2),
|
|
9854
|
+
"```",
|
|
9855
|
+
`Preview: kai dev <file>. Compile: kai compile <file>.`,
|
|
9856
|
+
...c.provider.mode === "endpoint" ? ["Backend route: use the scaffold tool — it emits a compiling route for your framework and wire."] : []
|
|
9857
|
+
].join("\n")
|
|
9858
|
+
}
|
|
9859
|
+
]
|
|
9860
|
+
};
|
|
9861
|
+
}
|
|
9862
|
+
};
|
|
8657
9863
|
function editDistance(a, b) {
|
|
8658
9864
|
const prev = new Array(b.length + 1);
|
|
8659
9865
|
for (let j2 = 0; j2 <= b.length; j2++) prev[j2] = j2;
|
|
@@ -8729,7 +9935,7 @@ Nothing was returned for this call — answering anyway could look like a reply
|
|
|
8729
9935
|
Expected arguments for ${toolName}:
|
|
8730
9936
|
${argLines.join("\n")}`;
|
|
8731
9937
|
}
|
|
8732
|
-
const tools = [reference, scaffold, theme, debug];
|
|
9938
|
+
const tools = [reference, scaffold, theme, debug, constructTool];
|
|
8733
9939
|
const PACKAGE_NAME = "@kitn.ai/ui";
|
|
8734
9940
|
function packageIdentity() {
|
|
8735
9941
|
const specifier = `${PACKAGE_NAME}/package.json`;
|