@kitn.ai/ui 0.25.2 → 0.26.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 +2 -2
- package/dist/components/attachment-types.d.ts +46 -1
- package/dist/components/attachments.d.ts +31 -0
- package/dist/components/audio-visualizer/fit-scale.d.ts +50 -0
- package/dist/components/audio-visualizer/index.d.ts +15 -1
- package/dist/components/audio-visualizer/variant-bar.d.ts +24 -1
- package/dist/components/chat-thread.d.ts +3 -41
- package/dist/components/code-block.d.ts +22 -0
- package/dist/components/conversation-item.d.ts +54 -1
- package/dist/components/conversation-list.d.ts +65 -0
- package/dist/components/form.d.ts +42 -0
- package/dist/components/message.d.ts +7 -0
- package/dist/components/toast.d.ts +2 -1
- package/dist/components/voice-input.d.ts +8 -0
- package/dist/components/voice-output.d.ts +12 -1
- package/dist/components/workspace-shell.d.ts +87 -0
- package/dist/{create-tween-B5Y4Im8G.js → create-tween-BFT6c-Y8.js} +1 -1
- package/dist/{create-tween-4z1XYLkR.js → create-tween-CJNlzoaO.js} +1 -1
- package/dist/{create-tween-CUzOlaMS.js → create-tween-CQED4T0g.js} +1 -1
- package/dist/custom-elements.json +2931 -2572
- package/dist/diagnostics/hook.d.ts +81 -0
- package/dist/diagnostics/index.d.ts +7 -0
- package/dist/diagnostics/report-request.d.ts +61 -0
- package/dist/diagnostics.d.ts +5 -0
- package/dist/diagnostics.js +209 -0
- 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/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/{Icon-b5A8hlHs.js → Icon--Y8vsFal.js} +1 -1
- package/dist/elements/chunks/action-icons-D9u3OzYW.js +1 -0
- package/dist/elements/chunks/arrow-left-CVh1TeN7.js +1 -0
- package/dist/elements/chunks/{artifact-BSuPYFHY.js → artifact-BkYcfIlZ.js} +1 -1
- package/dist/elements/chunks/attachments-DwfNlubq.js +1 -0
- package/dist/elements/chunks/audio-visualizer-D7lB6mvR.js +1 -0
- package/dist/elements/chunks/{badge-TIMiJFKc.js → badge-Q0-Cmwzz.js} +1 -1
- package/dist/elements/chunks/{button-B9okqG73.js → button-flUbeufF.js} +1 -1
- package/dist/elements/chunks/{card-renderer-CSoTL_e1.js → card-renderer-3Dyl-FpN.js} +1 -1
- package/dist/elements/chunks/check-B5kBPKeo.js +1 -0
- package/dist/elements/chunks/chevron-down-DZ1GS39h.js +1 -0
- package/dist/elements/chunks/chevron-right-CvP7OUd2.js +1 -0
- package/dist/elements/chunks/{choice-card-CGMc3kek.js → choice-card-D4QUIOWZ.js} +1 -1
- package/dist/elements/chunks/circle-CDf9g6MQ.js +1 -0
- package/dist/elements/chunks/circle-check-CA47Nqbz.js +1 -0
- package/dist/elements/chunks/{circle-x-mhH7pOK2.js → circle-x-CJ_HVbC8.js} +1 -1
- package/dist/elements/chunks/code-block-BdLiTitd.js +1 -0
- package/dist/elements/chunks/collapsible-BzbXpPzr.js +1 -0
- package/dist/elements/chunks/{composer-CZZmtYOT.js → composer-Byve-n8K.js} +2 -2
- package/dist/elements/chunks/{confirm-card-BWjf2-xo.js → confirm-card-B0flE3MN.js} +1 -1
- package/dist/elements/chunks/context-VLIL3sJ-.js +1 -0
- package/dist/elements/chunks/controllable-DTwTIlwp.js +1 -0
- package/dist/elements/chunks/conversation-item-BDztezOG.js +1 -0
- package/dist/elements/chunks/copy-Nf-kqxXH.js +1 -0
- package/dist/elements/chunks/{create-tween-CUrM6D_o.js → create-tween-c4Z8TDYz.js} +2 -2
- package/dist/elements/chunks/default-input-C2iLFq0b.js +1 -0
- package/dist/elements/chunks/define-wjHR1MkU.js +1 -0
- package/dist/elements/chunks/disclosure-CcNbyLy0.js +1 -0
- package/dist/elements/chunks/{download-DnCyWFJE.js → download-C4V7tBdb.js} +1 -1
- package/dist/elements/chunks/dropdown-BgWTthUx.js +1 -0
- package/dist/elements/chunks/{ellipsis-CVxqfsDZ.js → ellipsis-7gU33mL2.js} +1 -1
- package/dist/elements/chunks/embed-D3KewsIJ.js +1 -0
- package/dist/elements/chunks/{external-link-DG2sRdQL.js → external-link-CSOEsNlx.js} +1 -1
- package/dist/elements/chunks/{file-text-D0J9F6M7.js → file-text-D1G8khle.js} +1 -1
- package/dist/elements/chunks/{file-tree-yGWNK0Js.js → file-tree-zvB2i7P9.js} +1 -1
- package/dist/elements/chunks/{folder-NV1pi_ud.js → folder-WW2LGLid.js} +1 -1
- package/dist/elements/chunks/form-CQirdC4W.js +1 -0
- package/dist/elements/chunks/hover-card-CJJkfHTK.js +1 -0
- package/dist/elements/chunks/icon-Cfgmf4fa.js +1 -0
- package/dist/elements/chunks/{info-C8r5Q6cc.js → info-DX3YMOt6.js} +1 -1
- package/dist/elements/chunks/input-BtW9U6Sq.js +1 -0
- package/dist/elements/chunks/{kbd-OGJDUT-O.js → kbd-LGXwuT_V.js} +1 -1
- package/dist/elements/chunks/{link-BdsfLB7b.js → link-2My69BqT.js} +1 -1
- package/dist/elements/chunks/{link-preview-Cf5KOP-i.js → link-preview-OQ9IEq0f.js} +1 -1
- package/dist/elements/chunks/{loader-B5QzK_8_.js → loader-iOxc_Tu-.js} +1 -1
- package/dist/elements/chunks/{markdown-DbozyYqa.js → markdown-DgBFaGAz.js} +1 -1
- package/dist/elements/chunks/{message-DtXKPqiR.js → message-VMZohrXx.js} +1 -1
- package/dist/elements/chunks/message-circle-CpXMM-_n.js +1 -0
- package/dist/elements/chunks/message-square-DxcWtETQ.js +1 -0
- package/dist/elements/chunks/message-tZmV2A3p.js +1 -0
- package/dist/elements/chunks/{minimize-2-SegJ_oku.js → minimize-2-DPIUKTrT.js} +1 -1
- package/dist/elements/chunks/{model-switcher-D7JrFFHp.js → model-switcher-DsT8tFB0.js} +1 -1
- package/dist/elements/chunks/{overlay-CV7z2YuX.js → overlay-DuiwdXjm.js} +1 -1
- package/dist/elements/chunks/{paperclip-Bg0ITU3b.js → paperclip-CRZVp4z_.js} +1 -1
- package/dist/elements/chunks/{progress-bar-D1_rzt6N.js → progress-bar-DNy66e_E.js} +1 -1
- package/dist/elements/chunks/{prompt-suggestion-D5Sm8xF1.js → prompt-suggestion-Bn64Nl7L.js} +1 -1
- package/dist/elements/chunks/reasoning-Emn9N4-T.js +1 -0
- package/dist/elements/chunks/{resizable-Dpzjlvp4.js → resizable-D181b4fM.js} +1 -1
- package/dist/elements/chunks/{rotate-cw-DzVDJOA1.js → rotate-cw-CPWe37D9.js} +1 -1
- package/dist/elements/chunks/{scroll-area-93A1bbeG.js → scroll-area-ClZkZxIk.js} +1 -1
- package/dist/elements/chunks/scroll-button-DPE-t3x_.js +1 -0
- package/dist/elements/chunks/{separator-DH2r_ZSZ.js → separator-CeJHQqLW.js} +1 -1
- package/dist/elements/chunks/{settings-BFMOd6yB.js → settings-CqaHgJF9.js} +1 -1
- package/dist/elements/chunks/{settings-group-B3VZKIcz.js → settings-group-CjqjLCOn.js} +1 -1
- package/dist/elements/chunks/share-CyfnNEqp.js +1 -0
- package/dist/elements/chunks/{skeleton-N8BuWYmf.js → skeleton-D8akBnKS.js} +1 -1
- package/dist/elements/chunks/slots-DaNjX0rc.js +1 -0
- package/dist/elements/chunks/{source-w9V0NnBn.js → source-UWCvmVak.js} +1 -1
- package/dist/elements/chunks/{star-DHvACxtm.js → star-DoHFAikQ.js} +1 -1
- package/dist/elements/chunks/store-Cjq0qwYN.js +1 -0
- package/dist/elements/chunks/{tasks-card-lPxoodlA.js → tasks-card-CmZ5xh6v.js} +1 -1
- package/dist/elements/chunks/{text-shimmer-BY_rI3s5.js → text-shimmer-DYn09zrz.js} +1 -1
- package/dist/elements/chunks/textarea-skiGHURM.js +1 -0
- package/dist/elements/chunks/{thumbs-up-DQe0hvkZ.js → thumbs-up-DX2Ijk3H.js} +1 -1
- package/dist/elements/chunks/{toast-store-VUZZBhzH.js → toast-store-CFnQpThX.js} +1 -1
- package/dist/elements/chunks/{tool-C1YtxwPu.js → tool-DKQ2D1DP.js} +1 -1
- package/dist/elements/chunks/{tooltip-BqwnFWhn.js → tooltip-B0G99y8w.js} +1 -1
- package/dist/elements/chunks/{triangle-alert-B6or7F5u.js → triangle-alert-F9cyUEyy.js} +1 -1
- package/dist/elements/chunks/{use-card-resolution-DJg32jGH.js → use-card-resolution-DoA2f_EU.js} +1 -1
- package/dist/elements/chunks/{variant-aurora-QXTZPyST.js → variant-aurora-qt6Zct8M.js} +2 -2
- package/dist/elements/chunks/variant-custom-tJp22_BQ.js +1 -0
- package/dist/{variant-wave-BwzrHD1s.js → elements/chunks/variant-wave-CaQ5ti39.js} +2 -2
- package/dist/elements/chunks/x-B6UnOiLw.js +1 -0
- 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.d.ts +1 -0
- package/dist/elements/conversation-item.js +1 -0
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/default-input.d.ts +3 -3
- package/dist/elements/define.d.ts +27 -0
- package/dist/elements/diagnostic-events.d.ts +74 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/dock.d.ts +1 -0
- package/dist/elements/dock.js +149 -0
- package/dist/elements/dropdown.d.ts +1 -0
- package/dist/elements/dropdown.js +1 -0
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/element-diagnostics.d.ts +81 -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-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/reasoning.js +1 -1
- package/dist/elements/register.d.ts +2 -0
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +3 -3
- 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/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/slots.d.ts +35 -3
- 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 +232 -148
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5456 -4931
- package/dist/index.server.js +4142 -3669
- package/dist/kai-provider.es.js +40 -40
- package/dist/kai.es.js +1 -1
- package/dist/llms/llms-full.txt +184 -69
- package/dist/llms/llms.txt +3 -3
- package/dist/mcp.es.js +2669 -546
- package/dist/primitives/use-resize-observer.d.ts +23 -0
- package/dist/primitives/use-speech-recognition.d.ts +8 -0
- package/dist/primitives/use-text-stream.d.ts +5 -0
- package/dist/react/index.d.ts +115 -266
- package/dist/react.js +247 -221
- package/dist/register-impl-DpE7icIb.js +293 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/tool-defs.d.ts +35 -0
- package/dist/schemas.js +403 -310
- package/dist/{solid-BMuDmFo_.js → solid-BtdqNZI7.js} +8435 -7394
- package/dist/{solid-CGYNdLRR.js → solid-CnavwBye.js} +5987 -5035
- package/dist/solid.d.ts +5 -0
- package/dist/solid.js +210 -203
- package/dist/solid.server.js +210 -203
- package/dist/state/index.d.ts +4 -0
- package/dist/state/persistence.d.ts +51 -0
- package/dist/state/stream.d.ts +50 -0
- package/dist/state/threads.d.ts +71 -0
- package/dist/state.js +269 -169
- package/dist/types.d.ts +4 -2
- package/dist/ui/dock.d.ts +109 -0
- package/dist/ui/hover-card.d.ts +44 -0
- package/dist/{variant-aurora-DwSOfraQ.js → variant-aurora-B8II3-I1.js} +27 -27
- package/dist/{variant-aurora-wKvAQ5wp.js → variant-aurora-CMNWOSJR.js} +28 -28
- package/dist/{variant-aurora-XEkPT0_z.js → variant-aurora-CcuDMWwQ.js} +2 -2
- package/dist/{variant-custom-eFIg2XWQ.js → variant-custom-BdvYC7af.js} +40 -40
- package/dist/{variant-custom-BhS9wIpU.js → variant-custom-C714mqKl.js} +65 -65
- package/dist/variant-custom-L16JLLZy.js +1 -0
- package/dist/{elements/chunks/variant-wave-DDCwhLas.js → variant-wave-2S4La2Ka.js} +2 -2
- package/dist/{variant-wave-CbVsmgwT.js → variant-wave-D7ruiA1W.js} +25 -25
- package/dist/{variant-wave-CUWGx9Bn.js → variant-wave-ecwykeyL.js} +21 -21
- package/dist/wire/chunk.d.ts +47 -0
- package/dist/wire/diagnostics.d.ts +575 -0
- package/dist/wire/encode-probe.d.ts +70 -0
- package/dist/wire/encode.d.ts +24 -0
- package/dist/wire/index.d.ts +2 -0
- package/dist/wire/sse.d.ts +10 -2
- package/dist/wire.js +917 -480
- package/frameworks/react/index.tsx +132 -93
- package/llms-full.txt +184 -69
- package/llms.txt +3 -3
- package/package.json +26 -6
- package/src/agent-tooling/archetypes.ts +41 -9
- package/src/agent-tooling/catalog/README.md +676 -0
- package/src/agent-tooling/catalog/catalog-types.ts +231 -0
- package/src/agent-tooling/catalog/fabrications.ts +96 -0
- package/src/agent-tooling/catalog/invariants.ts +284 -0
- package/src/agent-tooling/catalog/labs-titles.ts +114 -0
- package/src/agent-tooling/catalog/scenarios.ts +87 -0
- package/src/agent-tooling/catalog/surfaces.ts +275 -0
- package/src/agent-tooling/integrations/anthropic.ts +7 -1
- package/src/agent-tooling/integrations/cloudflare.ts +43 -2
- package/src/agent-tooling/integrations/langgraph.ts +7 -1
- package/src/agent-tooling/integrations/mastra.ts +7 -1
- package/src/agent-tooling/integrations/mock.ts +92 -17
- package/src/agent-tooling/integrations/ollama.ts +7 -1
- package/src/agent-tooling/integrations/openai.ts +7 -1
- package/src/agent-tooling/integrations/openrouter.ts +7 -1
- package/src/agent-tooling/integrations/pi.ts +56 -1
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +7 -1
- package/src/agent-tooling/mcp/css-raw.d.ts +12 -0
- package/src/agent-tooling/mcp/manifest.ts +100 -1
- package/src/agent-tooling/mcp/server.ts +61 -5
- package/src/agent-tooling/mcp/tools/reference.ts +440 -4
- package/src/agent-tooling/mcp/tools/scaffold.ts +1834 -307
- package/src/agent-tooling/mcp/tools/theme.ts +146 -30
- package/src/agent-tooling/mcp/validate-args.ts +141 -0
- package/src/agent-tooling/registry.ts +19 -7
- package/src/agent-tooling/route-emit.ts +26 -2
- package/src/components/attachment-types.ts +47 -1
- package/src/components/attachments.tsx +169 -22
- package/src/components/audio-visualizer/fit-scale.ts +112 -0
- package/src/components/audio-visualizer/index.tsx +80 -3
- package/src/components/audio-visualizer/variant-aurora.tsx +10 -4
- package/src/components/audio-visualizer/variant-bar.tsx +38 -6
- package/src/components/audio-visualizer/variant-custom.tsx +8 -2
- package/src/components/audio-visualizer/variant-grid.tsx +16 -11
- package/src/components/audio-visualizer/variant-radial.tsx +15 -10
- package/src/components/audio-visualizer/variant-wave.tsx +7 -2
- package/src/components/chat-container.tsx +4 -1
- package/src/components/chat-thread.tsx +36 -7
- package/src/components/code-block.tsx +79 -3
- package/src/components/composer.tsx +8 -1
- package/src/components/conversation-item.tsx +114 -4
- package/src/components/conversation-list.tsx +212 -12
- package/src/components/embed.tsx +4 -0
- package/src/components/form.tsx +41 -14
- package/src/components/message.tsx +75 -5
- package/src/components/response-stream.tsx +13 -7
- package/src/components/screen.tsx +5 -0
- package/src/components/scroll-button.tsx +7 -0
- package/src/components/toast.tsx +11 -5
- package/src/components/voice-input.tsx +35 -3
- package/src/components/voice-output.tsx +46 -6
- package/src/components/workspace-shell.tsx +285 -0
- package/src/diagnostics/hook.ts +352 -0
- package/src/diagnostics/index.ts +82 -0
- package/src/diagnostics/report-request.ts +296 -0
- package/src/elements/attachments.tsx +6 -7
- package/src/elements/audio-visualizer.tsx +48 -4
- package/src/elements/chat-workspace.tsx +159 -287
- package/src/elements/chat.tsx +12 -9
- package/src/elements/code-block.tsx +19 -1
- package/src/elements/command.tsx +4 -1
- package/src/elements/compiled.css +1 -1
- package/src/elements/conversation-item.tsx +80 -0
- package/src/elements/conversation-list.tsx +60 -11
- package/src/elements/default-input.tsx +5 -5
- package/src/elements/define.tsx +168 -8
- package/src/elements/diagnostic-events.ts +114 -0
- package/src/elements/dialog.tsx +47 -0
- package/src/elements/disclosure.ts +34 -12
- package/src/elements/dock.tsx +204 -0
- package/src/elements/dropdown.tsx +123 -0
- package/src/elements/element-diagnostics.ts +392 -0
- package/src/elements/element-manifest.json +12 -0
- package/src/elements/element-meta.json +672 -439
- package/src/elements/element-nonscalar.json +170 -0
- package/src/elements/element-types.d.ts +232 -148
- package/src/elements/menu.tsx +13 -0
- package/src/elements/prompt-dock.tsx +2 -1
- package/src/elements/prompt-input.tsx +8 -8
- package/src/elements/register-impl.ts +46 -0
- package/src/elements/register.ts +28 -0
- package/src/elements/resizable.tsx +52 -25
- package/src/elements/scroll-button.tsx +5 -0
- package/src/elements/slots.ts +107 -12
- package/src/elements/styles.css +30 -6
- package/src/elements/thinking-bar.tsx +16 -7
- package/src/elements/toast.tsx +3 -0
- package/src/elements/tool.tsx +26 -5
- package/src/elements/voice-input.tsx +11 -2
- package/src/elements/voice-output.tsx +16 -3
- package/src/index.ts +2 -0
- package/src/primitives/use-resize-observer.ts +33 -0
- package/src/primitives/use-speech-recognition.ts +12 -1
- package/src/primitives/use-stick-to-bottom.ts +34 -2
- package/src/primitives/use-text-stream.ts +33 -2
- package/src/remote/provider-runtime.ts +6 -3
- package/src/schemas/index.ts +1 -0
- package/src/schemas/tool-defs.ts +297 -6
- package/src/solid.ts +5 -0
- package/src/state/index.ts +12 -0
- package/src/state/persistence.ts +180 -0
- package/src/state/stream.ts +71 -4
- package/src/state/threads.ts +142 -0
- package/src/types.ts +4 -2
- package/src/ui/dialog.tsx +6 -0
- package/src/ui/dock.tsx +445 -0
- package/src/ui/dropdown.tsx +67 -23
- package/src/ui/hover-card.tsx +156 -6
- package/src/ui/input.tsx +29 -10
- package/src/ui/resizable.tsx +11 -0
- package/src/wire/chunk.ts +47 -0
- package/src/wire/consume.ts +185 -26
- package/src/wire/diagnostics.ts +727 -0
- package/src/wire/encode-probe.ts +214 -0
- package/src/wire/encode.ts +267 -14
- package/src/wire/formats/anthropic.ts +8 -1
- package/src/wire/formats/openai.ts +5 -0
- package/src/wire/index.ts +22 -0
- package/src/wire/read.ts +250 -6
- package/src/wire/sse.ts +16 -3
- package/dist/elements/chunks/action-icons-DW9muWrY.js +0 -1
- package/dist/elements/chunks/arrow-left-iJVnQmxA.js +0 -1
- package/dist/elements/chunks/attachments-CL6TaqIb.js +0 -1
- package/dist/elements/chunks/audio-visualizer-BuUqQmaO.js +0 -1
- package/dist/elements/chunks/chat-thread-DxoKaStk.js +0 -1
- package/dist/elements/chunks/check-MdLzbxrm.js +0 -1
- package/dist/elements/chunks/chevron-down-MK7IzDqe.js +0 -1
- package/dist/elements/chunks/chevron-right-C5jXhmtV.js +0 -1
- package/dist/elements/chunks/circle-BG7VkPep.js +0 -1
- package/dist/elements/chunks/circle-check-GqvO2UBs.js +0 -1
- package/dist/elements/chunks/code-block-ClhQO9RW.js +0 -1
- package/dist/elements/chunks/collapsible-DBcBkFPh.js +0 -1
- package/dist/elements/chunks/context-hKjNqiyQ.js +0 -1
- package/dist/elements/chunks/conversation-list-CsGJjSKG.js +0 -1
- package/dist/elements/chunks/default-input-B1Mem5wu.js +0 -1
- package/dist/elements/chunks/define-B_tXMYRm.js +0 -1
- package/dist/elements/chunks/disclosure-DAoYOKew.js +0 -1
- package/dist/elements/chunks/dropdown-CmNna5lT.js +0 -1
- package/dist/elements/chunks/embed-CjAD2jSi.js +0 -1
- package/dist/elements/chunks/form-Cp8Y00Wn.js +0 -1
- package/dist/elements/chunks/hover-card-6KtXpLwT.js +0 -1
- package/dist/elements/chunks/icon-97R8SR2p.js +0 -1
- package/dist/elements/chunks/input-DrseEjhp.js +0 -1
- package/dist/elements/chunks/message-CE4k-qaj.js +0 -1
- package/dist/elements/chunks/message-square-C9ovmtOo.js +0 -1
- package/dist/elements/chunks/reasoning-BrxQfpfI.js +0 -1
- package/dist/elements/chunks/scroll-button-CrTo7wm4.js +0 -1
- package/dist/elements/chunks/share-DgNbkJKY.js +0 -1
- package/dist/elements/chunks/slots-CIw9RlAe.js +0 -1
- package/dist/elements/chunks/store-HIPGhNz9.js +0 -1
- package/dist/elements/chunks/textarea-DPfuhswM.js +0 -1
- package/dist/elements/chunks/variant-custom-D5ZlmziF.js +0 -1
- package/dist/elements/chunks/video-Bb86fVYI.js +0 -1
- package/dist/elements/chunks/x-BOhjBeOd.js +0 -1
- package/dist/primitives/card-validate-generator.testlib.d.ts +0 -20
- package/dist/register-impl-DGmGzrEM.js +0 -145
- package/dist/variant-custom-MkTNTycd.js +0 -1
- package/src/primitives/card-validate-generator.testlib.ts +0 -38
package/src/schemas/tool-defs.ts
CHANGED
|
@@ -27,9 +27,14 @@
|
|
|
27
27
|
// `format: "uri"` (link/embed/artifact), `minimum` (tasks/embed/artifact),
|
|
28
28
|
// `maxItems` (confirm/artifact), `pattern` (embed), `if`/`then` (embed), `oneOf`
|
|
29
29
|
// (artifact), a free-form object (form) and untyped `payload`/`allowOther` members
|
|
30
|
-
// (confirm/choice). Non-strict is therefore
|
|
31
|
-
//
|
|
32
|
-
//
|
|
30
|
+
// (confirm/choice). Non-strict is therefore the default and the mode the
|
|
31
|
+
// five-configuration conformance spike actually proved cards in — but since F-20 it
|
|
32
|
+
// is no longer a raw pass-through either: a root combinator (`anyOf` on artifact,
|
|
33
|
+
// `allOf` on embed) gets HTTP 400 from OpenAI and Anthropic even non-strict, so the
|
|
34
|
+
// non-strict projection relaxes those root combinators LOUDLY (constraint restated in
|
|
35
|
+
// the description, console.warn names the relaxation, registry.validate still
|
|
36
|
+
// enforces at render time). The jsonschema projection stays byte-faithful to the
|
|
37
|
+
// authored schema.
|
|
33
38
|
//
|
|
34
39
|
// The alternative was to silently drop the offending keywords. That is rejected for
|
|
35
40
|
// the reason the plan gives: a dropped `maxItems: 4` means the model emits six
|
|
@@ -126,6 +131,42 @@ export interface CardToolOptions<P extends ToolProvider = ToolProvider> {
|
|
|
126
131
|
* When it throws it names the card, the path and the keyword.
|
|
127
132
|
*/
|
|
128
133
|
readonly strict?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Narrow the DERIVED tool schema per card type, after projection (F-23).
|
|
136
|
+
*
|
|
137
|
+
* A card's authored schema is deliberately permissive — `files` is optional on an
|
|
138
|
+
* artifact, its items need only a `path` — because the card CONTRACT must accept
|
|
139
|
+
* what a host may legitimately render. A model filling the tool in, though, is
|
|
140
|
+
* often better served by a tighter tool: requiring `code` on every file, or at
|
|
141
|
+
* least one file at all, moves a class of failures from render time (a card the
|
|
142
|
+
* registry rejects) to generation time (the model literally cannot emit one).
|
|
143
|
+
*
|
|
144
|
+
* This narrows the WIRE projection only — the authored schema and
|
|
145
|
+
* `registry.validate()` are untouched, so a hand-built envelope still validates.
|
|
146
|
+
* Applied for every provider, `jsonschema` included, and merged loudly: an
|
|
147
|
+
* unknown path or a required name the node has no property for is a TypeError
|
|
148
|
+
* naming the card, the path and the field, because a tool that can never be
|
|
149
|
+
* satisfied must not ship quietly.
|
|
150
|
+
*/
|
|
151
|
+
readonly require?: Readonly<Record<string, readonly CardRequireRule[]>>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* One narrowing instruction for one node of one card's projected schema.
|
|
156
|
+
*
|
|
157
|
+
* `path` is a dot-path from the envelope-data root through `properties` keys
|
|
158
|
+
* (`''` names the root node itself). On an ARRAY node, `required` narrows
|
|
159
|
+
* `items.required` and `minItems` lands on the array; on an object node,
|
|
160
|
+
* `required` lands on the node's own `required`. Existing requirements merge,
|
|
161
|
+
* never replace.
|
|
162
|
+
*/
|
|
163
|
+
export interface CardRequireRule {
|
|
164
|
+
/** Dot-path from the envelope-data root to a schema node, e.g. 'files'. */
|
|
165
|
+
readonly path: string;
|
|
166
|
+
/** Property names to require. On an ARRAY node this narrows `items.required`; on an object node, the node's own `required`. */
|
|
167
|
+
readonly required?: readonly string[];
|
|
168
|
+
/** Sets `minItems` (array nodes only). */
|
|
169
|
+
readonly minItems?: number;
|
|
129
170
|
}
|
|
130
171
|
|
|
131
172
|
/**
|
|
@@ -225,7 +266,7 @@ function formatUnsupported(subset: ProviderSubset, cards: readonly UnsupportedCa
|
|
|
225
266
|
lines.push('');
|
|
226
267
|
lines.push(` subset source: ${subset.source} (read ${subset.checkedOn})`);
|
|
227
268
|
lines.push(
|
|
228
|
-
' Fix: drop `strict: true`. Non-strict
|
|
269
|
+
' Fix: drop `strict: true`. Non-strict mode projects a provider-valid schema (root combinators are relaxed with the constraint restated in the description and still enforced by card validation). Rewriting a built-in card schema to fit a strict subset would change what a valid card IS, which is a card-contract change, not a projection setting.',
|
|
229
270
|
);
|
|
230
271
|
return lines.join('\n');
|
|
231
272
|
}
|
|
@@ -237,6 +278,227 @@ function formatUnsupported(subset: ProviderSubset, cards: readonly UnsupportedCa
|
|
|
237
278
|
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
238
279
|
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
239
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Root combinators the two largest providers refuse on a tool schema's root node
|
|
283
|
+
* (measured: OpenAI and Anthropic 400 before the model runs — findings F-20;
|
|
284
|
+
* DeepSeek's OpenAI-compatible route requires the same workaround). The AUTHORED
|
|
285
|
+
* schemas keep them — card validation keeps every constraint. Only the WIRE
|
|
286
|
+
* projection relaxes, loudly: the constraint is restated in the description the
|
|
287
|
+
* model reads, a console.warn names the relaxation, and a violating envelope
|
|
288
|
+
* still fails registry.validate() at render time with a hard diagnostic.
|
|
289
|
+
*/
|
|
290
|
+
const ROOT_RELAXATIONS: Readonly<Record<string, string>> = {
|
|
291
|
+
artifact:
|
|
292
|
+
'Provide `src` (a preview URL) or `files` — at least one. An envelope with neither is rejected.',
|
|
293
|
+
embed:
|
|
294
|
+
"For provider 'generic', include `url`. For 'youtube'/'vimeo', include `id` or `url`.",
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** Strip a banned combinator key from the projected ROOT node only (nested
|
|
298
|
+
* combinators are accepted by every provider measured and stay). Returns the
|
|
299
|
+
* relaxation note to restate in the TOOL description the model reads. Two
|
|
300
|
+
* distinct no-note return paths: `undefined` when NOTHING was banned (the wire
|
|
301
|
+
* projection is faithful, nothing was widened), and GENERIC_NOTE when something
|
|
302
|
+
* WAS banned but no curated copy exists (a custom card) — that path still
|
|
303
|
+
* restates the relaxation, because deleting a combinator without telling the
|
|
304
|
+
* model would be a silent widening. */
|
|
305
|
+
const GENERIC_RELAXATION_NOTE =
|
|
306
|
+
'The root constraint of this schema was relaxed for this provider; consult the card registry for what a valid payload is.';
|
|
307
|
+
|
|
308
|
+
const BANNED_ROOT_KEYS = ['anyOf', 'allOf', 'oneOf', 'not', 'enum', 'const'] as const;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The object shape a literal `enum` / `const` VALUE implies.
|
|
312
|
+
*
|
|
313
|
+
* `const: { op: 'ping' }` is a whole-instance constraint, which a provider refuses
|
|
314
|
+
* at the root exactly like a combinator. The instance it admits is an object, so
|
|
315
|
+
* the shape survives the relaxation as `properties: { op: { const: 'ping' } }` with
|
|
316
|
+
* `op` required — nothing widened that the branch did not already allow.
|
|
317
|
+
*/
|
|
318
|
+
function branchFromLiteral(value: unknown): Record<string, unknown> | null {
|
|
319
|
+
if (!isRecord(value)) return null;
|
|
320
|
+
const properties: Record<string, unknown> = {};
|
|
321
|
+
for (const [key, member] of Object.entries(value)) properties[key] = { const: member };
|
|
322
|
+
return { type: 'object', properties, required: Object.keys(value) };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The branch schemas one banned root keyword contributes, or `[]` for one that has none. */
|
|
326
|
+
function branchesOfRootKeyword(keyword: string, value: unknown): Record<string, unknown>[] {
|
|
327
|
+
if (keyword === 'anyOf' || keyword === 'oneOf' || keyword === 'allOf') {
|
|
328
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
329
|
+
}
|
|
330
|
+
if (keyword === 'const') return [branchFromLiteral(value)].filter((b): b is Record<string, unknown> => b !== null);
|
|
331
|
+
if (keyword === 'enum') {
|
|
332
|
+
return Array.isArray(value)
|
|
333
|
+
? value.map(branchFromLiteral).filter((b): b is Record<string, unknown> => b !== null)
|
|
334
|
+
: [];
|
|
335
|
+
}
|
|
336
|
+
// `not` is a NEGATIVE constraint: nothing it says is a property the instance may
|
|
337
|
+
// carry, so there is nothing sound to merge back. It is dropped and restated in
|
|
338
|
+
// the description like every other relaxation.
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Fold a root combinator's branches back into the object root that replaces it.
|
|
344
|
+
*
|
|
345
|
+
* The two built-ins showed the shape of the rule and this generalises it rather
|
|
346
|
+
* than special-casing them. `artifact`'s root `anyOf` is `[{required:['src']},
|
|
347
|
+
* {required:['files']}]`: a DISJUNCTION guarantees only what EVERY branch
|
|
348
|
+
* guarantees, so the intersection is empty and `required` stays absent — which is
|
|
349
|
+
* exactly what the artifact projection already did. `embed`'s root `allOf` is two
|
|
350
|
+
* `if`/`then` pairs: a CONJUNCTION guarantees every branch's own top-level
|
|
351
|
+
* `required`, and an `if`/`then` branch declares none, so again nothing is added.
|
|
352
|
+
* A consumer card whose branches carry real `properties` is the case neither
|
|
353
|
+
* built-in reaches, and the one this exists for.
|
|
354
|
+
*
|
|
355
|
+
* Properties are UNIONed (each branch's shape must remain expressible — dropping
|
|
356
|
+
* one is the silent narrowing the whole F-20 fix exists to avoid); a name already
|
|
357
|
+
* on the root keeps the root's definition. `required` is the intersection for
|
|
358
|
+
* `anyOf`/`oneOf`/`enum`/`const` and the union for `allOf`, unioned in turn with
|
|
359
|
+
* whatever the root already required, and filtered to names the merged
|
|
360
|
+
* `properties` actually carries so the tool can never be unsatisfiable.
|
|
361
|
+
*/
|
|
362
|
+
function mergeBranchesIntoRoot(parameters: Record<string, unknown>, banned: readonly string[]): void {
|
|
363
|
+
const properties: Record<string, unknown> = isRecord(parameters.properties)
|
|
364
|
+
? { ...parameters.properties }
|
|
365
|
+
: {};
|
|
366
|
+
const guaranteed = new Set<string>(
|
|
367
|
+
Array.isArray(parameters.required) ? parameters.required.map(String) : [],
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
for (const keyword of banned) {
|
|
371
|
+
const branches = branchesOfRootKeyword(keyword, parameters[keyword]);
|
|
372
|
+
if (branches.length === 0) continue;
|
|
373
|
+
|
|
374
|
+
for (const branch of branches) {
|
|
375
|
+
if (!isRecord(branch.properties)) continue;
|
|
376
|
+
for (const [name, sub] of Object.entries(branch.properties)) {
|
|
377
|
+
if (!(name in properties)) properties[name] = sub;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const perBranch = branches.map(
|
|
382
|
+
(branch) => new Set(Array.isArray(branch.required) ? branch.required.map(String) : []),
|
|
383
|
+
);
|
|
384
|
+
if (keyword === 'allOf') {
|
|
385
|
+
for (const set of perBranch) for (const name of set) guaranteed.add(name);
|
|
386
|
+
} else {
|
|
387
|
+
// Disjunction: keep only what every branch requires.
|
|
388
|
+
const [first, ...rest] = perBranch;
|
|
389
|
+
for (const name of first) {
|
|
390
|
+
if (rest.every((set) => set.has(name))) guaranteed.add(name);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (Object.keys(properties).length > 0) parameters.properties = properties;
|
|
396
|
+
const required = [...guaranteed].filter((name) => name in properties);
|
|
397
|
+
if (required.length > 0) parameters.required = required;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Make the projected ROOT the object shape every provider demands.
|
|
402
|
+
*
|
|
403
|
+
* Deleting a root combinator is only half of F-20. A schema whose root was NOTHING
|
|
404
|
+
* BUT the combinator projects to `{"title":…,"description":…}` once it is gone —
|
|
405
|
+
* accepted by no provider (measured: OpenAI 400, "schema must be a JSON Schema of
|
|
406
|
+
* type \"object\"") and unfillable by any model. Every built-in declares
|
|
407
|
+
* `type: "object"` at its root, so nothing in the suite reached this until a
|
|
408
|
+
* consumer card did.
|
|
409
|
+
*
|
|
410
|
+
* A root that declares a NON-object type cannot be a tool schema at all, and
|
|
411
|
+
* asserting `type: "object"` over it would be a lie about the card. That refuses,
|
|
412
|
+
* naming the card.
|
|
413
|
+
*/
|
|
414
|
+
function ensureObjectRoot(parameters: Record<string, unknown>, cardType: string): void {
|
|
415
|
+
const declared = parameters.type;
|
|
416
|
+
if (declared !== undefined && declared !== 'object') {
|
|
417
|
+
throw new TypeError(
|
|
418
|
+
`cardTools: the '${cardType}' card schema declares root type '${String(declared)}', but a provider tool ` +
|
|
419
|
+
`schema root must be an object (OpenAI and Anthropic reject anything else with HTTP 400). ` +
|
|
420
|
+
`Wrap the payload in an object, or project this card with provider 'jsonschema'.`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
parameters.type = 'object';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function relaxRootCombinators(parameters: Record<string, unknown>, cardType: string): string | undefined {
|
|
427
|
+
const note = ROOT_RELAXATIONS[cardType];
|
|
428
|
+
const banned = BANNED_ROOT_KEYS.filter((k) => k in parameters);
|
|
429
|
+
// The object-root guarantee holds for EVERY provider projection, banned keys or
|
|
430
|
+
// not: it is what the provider requires, not what the relaxation broke.
|
|
431
|
+
ensureObjectRoot(parameters, cardType);
|
|
432
|
+
if (banned.length === 0) return undefined;
|
|
433
|
+
mergeBranchesIntoRoot(parameters, banned);
|
|
434
|
+
for (const k of banned) delete parameters[k];
|
|
435
|
+
// eslint-disable-next-line no-console
|
|
436
|
+
console.warn(
|
|
437
|
+
`[kai-card-tools] relaxed root ${banned.join('/')} on the projected '${toolNameForCardType(cardType)}' tool schema ` +
|
|
438
|
+
`for this provider (it would be refused with HTTP 400); the constraint now lives in the ` +
|
|
439
|
+
`description and is still enforced by card validation.`,
|
|
440
|
+
);
|
|
441
|
+
return note ?? GENERIC_RELAXATION_NOTE;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Apply one card's `require` rules to its PROJECTED parameters (F-23).
|
|
446
|
+
*
|
|
447
|
+
* Runs after `project()` and `relaxRootCombinators()`, on the copy the projection
|
|
448
|
+
* built — never on the authored schema, which `registry.validate()` keeps reading.
|
|
449
|
+
* Merges into whatever `required` already exists rather than replacing it, so a
|
|
450
|
+
* rule can only ever NARROW what the model may emit, never widen it.
|
|
451
|
+
*/
|
|
452
|
+
function applyRequire(
|
|
453
|
+
parameters: Record<string, unknown>,
|
|
454
|
+
cardType: string,
|
|
455
|
+
rules: readonly CardRequireRule[],
|
|
456
|
+
): void {
|
|
457
|
+
for (const rule of rules) {
|
|
458
|
+
// Resolve the dot-path through `properties` keys. `''` names the root node;
|
|
459
|
+
// every other segment must name a property of the node reached so far.
|
|
460
|
+
let node: Record<string, unknown> = parameters;
|
|
461
|
+
if (rule.path !== '') {
|
|
462
|
+
for (const segment of rule.path.split('.')) {
|
|
463
|
+
const props = isRecord(node.properties) ? node.properties : undefined;
|
|
464
|
+
const next = props?.[segment];
|
|
465
|
+
if (!isRecord(next)) {
|
|
466
|
+
throw new TypeError(
|
|
467
|
+
`cardTools: require on '${cardType}': path '${rule.path}' does not resolve in the schema`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
node = next;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const isArrayNode = node.type === 'array' || (node.type === undefined && 'items' in node);
|
|
475
|
+
if (isArrayNode && rule.minItems !== undefined) node.minItems = rule.minItems;
|
|
476
|
+
if (!isArrayNode && rule.minItems !== undefined) {
|
|
477
|
+
throw new TypeError(
|
|
478
|
+
`cardTools: minItems at require path '${rule.path}' applies to array nodes only, ` +
|
|
479
|
+
`but '${rule.path}' in the '${cardType}' schema is not one`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (rule.required !== undefined) {
|
|
484
|
+
// On an array node the requirement lives on the ITEMS (each element must carry
|
|
485
|
+
// the field), not on the array itself.
|
|
486
|
+
const target = isArrayNode && isRecord(node.items) ? node.items : node;
|
|
487
|
+
const props = isRecord(target.properties) ? target.properties : undefined;
|
|
488
|
+
for (const field of rule.required) {
|
|
489
|
+
if (!props || !(field in props)) {
|
|
490
|
+
throw new TypeError(
|
|
491
|
+
`cardTools: require on '${cardType}' at path '${rule.path}': '${field}' is not a property of that node — ` +
|
|
492
|
+
`a required name with no property would make the tool unsatisfiable`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const existing = Array.isArray(target.required) ? target.required.map(String) : [];
|
|
497
|
+
target.required = [...new Set([...existing, ...rule.required])];
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
240
502
|
/**
|
|
241
503
|
* Keywords stripped from every projection, strict or not.
|
|
242
504
|
*
|
|
@@ -430,11 +692,40 @@ export function cardTools(a: CardToolInput | CardToolOptions, b?: CardToolOption
|
|
|
430
692
|
const defs: ToolDef[] = [];
|
|
431
693
|
const failures: UnsupportedCardTool[] = [];
|
|
432
694
|
|
|
695
|
+
// A `require` key naming a card the call does not carry is a typo, and a typo here
|
|
696
|
+
// would otherwise narrow NOTHING while looking like it narrowed something — the
|
|
697
|
+
// same decide-loudly class as an unknown dot-path below.
|
|
698
|
+
if (opts.require !== undefined) {
|
|
699
|
+
for (const key of Object.keys(opts.require)) {
|
|
700
|
+
if (!(key in schemas)) {
|
|
701
|
+
throw new TypeError(
|
|
702
|
+
`cardTools: require names card type '${key}', which this call does not carry ` +
|
|
703
|
+
`(carries: ${Object.keys(schemas).join(', ') || 'none'})`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
433
709
|
for (const [cardType, schema] of Object.entries(schemas)) {
|
|
434
710
|
const name = toolNameForCardType(cardType);
|
|
435
711
|
const description = describe(cardType, schema, descriptions);
|
|
436
712
|
const parameters = project(schema, projectOptions) as ToolParameters;
|
|
437
713
|
|
|
714
|
+
// F-20: non-strict only. Strict mode already refuses these cards with the full
|
|
715
|
+
// subset check, so the relaxation must never mask it. The note is restated in the
|
|
716
|
+
// TOOL description — the envelope field the model reads when choosing the tool.
|
|
717
|
+
const relaxedNote =
|
|
718
|
+
subset === null && opts.provider !== 'jsonschema'
|
|
719
|
+
? relaxRootCombinators(parameters as Record<string, unknown>, cardType)
|
|
720
|
+
: undefined;
|
|
721
|
+
const wireDescription = relaxedNote ? `${description} ${relaxedNote}` : description;
|
|
722
|
+
|
|
723
|
+
// F-23: every provider, jsonschema included — a consumer handing the bare schema
|
|
724
|
+
// to the Vercel AI SDK narrows their tool the same way. Runs BEFORE the strict
|
|
725
|
+
// subset check so that check judges the schema that actually ships.
|
|
726
|
+
const requireRules = opts.require?.[cardType];
|
|
727
|
+
if (requireRules !== undefined) applyRequire(parameters as Record<string, unknown>, cardType, requireRules);
|
|
728
|
+
|
|
438
729
|
if (subset) {
|
|
439
730
|
const violations = checkProviderSubset(parameters, subset);
|
|
440
731
|
if (violations.length > 0) {
|
|
@@ -444,9 +735,9 @@ export function cardTools(a: CardToolInput | CardToolOptions, b?: CardToolOption
|
|
|
444
735
|
}
|
|
445
736
|
|
|
446
737
|
if (opts.provider === 'openai') {
|
|
447
|
-
defs.push({ type: 'function', function: { name, description, parameters, ...(strict ? { strict: true as const } : {}) } });
|
|
738
|
+
defs.push({ type: 'function', function: { name, description: wireDescription, parameters, ...(strict ? { strict: true as const } : {}) } });
|
|
448
739
|
} else if (opts.provider === 'anthropic') {
|
|
449
|
-
defs.push({ name, description, input_schema: parameters, ...(strict ? { strict: true as const } : {}) });
|
|
740
|
+
defs.push({ name, description: wireDescription, input_schema: parameters, ...(strict ? { strict: true as const } : {}) });
|
|
450
741
|
} else {
|
|
451
742
|
defs.push({ name, description, schema: parameters });
|
|
452
743
|
}
|
package/src/solid.ts
CHANGED
|
@@ -69,6 +69,8 @@ export { EditableLabel } from './ui/editable-label';
|
|
|
69
69
|
export type { EditableLabelProps } from './ui/editable-label';
|
|
70
70
|
export { Dialog } from './ui/dialog';
|
|
71
71
|
export type { DialogProps, DialogController } from './ui/dialog';
|
|
72
|
+
export { Dock, DockLauncherGlyph, DockCloseGlyph } from './ui/dock';
|
|
73
|
+
export type { DockProps, DockController, DockPosition, DockFocusOnOpen } from './ui/dock';
|
|
72
74
|
export { Popover } from './ui/popover';
|
|
73
75
|
export type { PopoverProps, PopoverController } from './ui/popover';
|
|
74
76
|
export { Nav } from './ui/nav';
|
|
@@ -148,3 +150,6 @@ export type {
|
|
|
148
150
|
AudioVisualizerProps, VisualizerVariant, ShaderSpec, ShaderVariantProps,
|
|
149
151
|
} from './components/audio-visualizer';
|
|
150
152
|
export type { VisualizerSize } from './components/audio-visualizer/sizes';
|
|
153
|
+
export { WorkspaceShell, type WorkspaceShellProps, type WorkspaceShellController, type WorkspaceAsideSide, type WorkspaceAsideToggleDetail, type WorkspaceAsideResizeDetail } from './components/workspace-shell';
|
|
154
|
+
export { createConversationItemsController, readConversationItemId } from './components/conversation-list';
|
|
155
|
+
export type { ConversationItemsController, ConversationItemsControllerOptions } from './components/conversation-list';
|
package/src/state/index.ts
CHANGED
|
@@ -7,6 +7,18 @@ export type { SetMessages, AssistantStream } from './stream';
|
|
|
7
7
|
export { appendTextPart, appendReasoningPart, upsertToolPart, upsertCardPart, fingerprint } from './parts';
|
|
8
8
|
export type { ReasoningOpts } from './parts';
|
|
9
9
|
|
|
10
|
+
// Multi-thread mechanics (the workspace recast, spec §3b): the id-bound
|
|
11
|
+
// SetMessages fold + the in-flight-turn bookkeeping. The consumer's record
|
|
12
|
+
// shape and every persistence/retention decision stay outside — see ./threads.
|
|
13
|
+
export { updateThreadMessages, bindThreadMessages, createThreadSessions } from './threads';
|
|
14
|
+
export type { ThreadLike, SetThreads, BindThreadOptions, ThreadSessions } from './threads';
|
|
15
|
+
|
|
16
|
+
// The persistence SEAM, not persistence: the stored-thread validator (F-18,
|
|
17
|
+
// variant list derived from the MessagePart union) and the debounce/flush
|
|
18
|
+
// shape. No storage backend, no default delay — policy is the consumer's.
|
|
19
|
+
export { parseStoredThread, createSaveScheduler } from './persistence';
|
|
20
|
+
export type { ParsedThread, DroppedStored, SaveScheduler, SaveSchedulerOptions } from './persistence';
|
|
21
|
+
|
|
10
22
|
// The zero-config mock. Produces SSE frames rather than folding parts directly,
|
|
11
23
|
// so the no-backend preview runs through the SAME parser a real provider does —
|
|
12
24
|
// and is deliberately impossible to mistake for one. See ./mock.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// src/state/persistence.ts — the mechanical halves of the persistence story.
|
|
2
|
+
//
|
|
3
|
+
// The boundary (spec §1, CLAUDE.md): persistence POLICY — what is stored,
|
|
4
|
+
// where, for how long, under what quota — is the app's. What lives here is
|
|
5
|
+
// only the mechanics every such app re-derives by hand: validating a stored
|
|
6
|
+
// thread back into `ChatMessage[]` (F-18), and the debounce/flush shape that
|
|
7
|
+
// keeps a per-token stream from hitting storage per token. No storage call,
|
|
8
|
+
// no fetch, no DOM; the save fn, the delay, and the reaction to a drop are
|
|
9
|
+
// all the consumer's.
|
|
10
|
+
import type { ChatMessage, FeedbackVote, MessagePart } from '../elements/chat-types';
|
|
11
|
+
|
|
12
|
+
/** One record the parse refused, and why. Deciding loudly is the caller's
|
|
13
|
+
* verb (warn, reject the whole record, count it); this is the information
|
|
14
|
+
* that a quiet version would withhold. */
|
|
15
|
+
export interface DroppedStored {
|
|
16
|
+
at: 'thread' | 'message' | 'part';
|
|
17
|
+
reason: string;
|
|
18
|
+
value: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ParsedThread {
|
|
22
|
+
messages: ChatMessage[];
|
|
23
|
+
/** Empty on a clean parse. Never silently shorter output than input. */
|
|
24
|
+
dropped: DroppedStored[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
28
|
+
return typeof v === 'object' && v !== null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One payload check per `MessagePart` variant. The key set is DERIVED from the
|
|
33
|
+
* union in `src/elements/chat-types.ts` at compile time: `satisfies
|
|
34
|
+
* Record<MessagePart['type'], …>` makes a missing key (a 7th variant landed)
|
|
35
|
+
* and a stale key (a variant renamed away) each a type error in THIS file —
|
|
36
|
+
* the same union `lint:silent-drops` and `verify:scaffold` read, so the lists
|
|
37
|
+
* cannot disagree. The behavioral cross-check lives in ./persistence.test.ts,
|
|
38
|
+
* which re-reads the union by a deliberately different method.
|
|
39
|
+
*
|
|
40
|
+
* These are shape checks, not a trust boundary for rendering: a rehydrated
|
|
41
|
+
* part is model output twice over (it was model output before it was
|
|
42
|
+
* hand-editable storage), and it stays safe to draw because the elements are
|
|
43
|
+
* what draw it. What this buys is that a truncated or half-written record
|
|
44
|
+
* reaches the element as a well-formed thread minus the broken piece, instead
|
|
45
|
+
* of as `parts: undefined` taking the whole app down on first render.
|
|
46
|
+
*/
|
|
47
|
+
const PART_VALIDATORS = {
|
|
48
|
+
text: (v) => typeof v.text === 'string',
|
|
49
|
+
reasoning: (v) => typeof v.text === 'string',
|
|
50
|
+
tool: (v) => isRecord(v.tool),
|
|
51
|
+
card: (v) => isRecord(v.envelope) && typeof v.envelope.type === 'string' && typeof v.envelope.id === 'string',
|
|
52
|
+
source: (v) => isRecord(v.source),
|
|
53
|
+
file: (v) => isRecord(v.attachment),
|
|
54
|
+
} satisfies Record<MessagePart['type'], (v: Record<string, unknown>) => boolean>;
|
|
55
|
+
|
|
56
|
+
function parsePart(v: unknown, dropped: DroppedStored[]): MessagePart | null {
|
|
57
|
+
if (!isRecord(v) || typeof v.type !== 'string') {
|
|
58
|
+
dropped.push({ at: 'part', reason: 'not a part record', value: v });
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const validate = (PART_VALIDATORS as Record<string, (v: Record<string, unknown>) => boolean>)[v.type];
|
|
62
|
+
if (!validate) {
|
|
63
|
+
// An unknown `type` is dropped rather than passed on: every renderer and
|
|
64
|
+
// encoder switches on it and has no branch for a made-up one.
|
|
65
|
+
dropped.push({ at: 'part', reason: `unknown part type "${v.type}"`, value: v });
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!validate(v)) {
|
|
69
|
+
dropped.push({ at: 'part', reason: `missing or invalid payload for "${v.type}" part`, value: v });
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// Verbatim, not rebuilt: optional fields the variants carry (`raw`,
|
|
73
|
+
// `signature`, `index`, …) are load-bearing on the wire re-encode, so the
|
|
74
|
+
// validated record passes through whole.
|
|
75
|
+
return v as unknown as MessagePart;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseMessage(v: unknown, dropped: DroppedStored[]): ChatMessage | null {
|
|
79
|
+
if (!isRecord(v) || typeof v.id !== 'string' || (v.role !== 'user' && v.role !== 'assistant') || !Array.isArray(v.parts)) {
|
|
80
|
+
dropped.push({ at: 'message', reason: 'not a message record (id/role/parts)', value: v });
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const parts = v.parts
|
|
84
|
+
.map((p) => parsePart(p, dropped))
|
|
85
|
+
.filter((p): p is MessagePart => p !== null);
|
|
86
|
+
if (parts.length === 0) {
|
|
87
|
+
// A message whose every part was unreadable is an empty bubble, and the
|
|
88
|
+
// per-part entries above already say what was lost.
|
|
89
|
+
dropped.push({ at: 'message', reason: 'no readable parts survived', value: v });
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const message: ChatMessage = { id: v.id, role: v.role, parts };
|
|
93
|
+
if (v.feedback === 'like' || v.feedback === 'dislike') message.feedback = v.feedback as FeedbackVote;
|
|
94
|
+
if (isRecord(v.avatar)) message.avatar = v.avatar;
|
|
95
|
+
if (Array.isArray(v.actions)) message.actions = v.actions as ChatMessage['actions'];
|
|
96
|
+
return message;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate a stored thread — the JSON-parsed value of one thread's messages
|
|
101
|
+
* array — back into `ChatMessage[]`. Never throws: unreadable records are
|
|
102
|
+
* DROPPED and reported in `dropped`, so a truncated write loses one record,
|
|
103
|
+
* not every conversation. What to DO about a drop (warn, discard the whole
|
|
104
|
+
* record, telemetry) is the caller's policy.
|
|
105
|
+
*/
|
|
106
|
+
export function parseStoredThread(value: unknown): ParsedThread {
|
|
107
|
+
const dropped: DroppedStored[] = [];
|
|
108
|
+
if (!Array.isArray(value)) {
|
|
109
|
+
return { messages: [], dropped: [{ at: 'thread', reason: 'stored thread is not an array', value }] };
|
|
110
|
+
}
|
|
111
|
+
const messages = value
|
|
112
|
+
.map((m) => parseMessage(m, dropped))
|
|
113
|
+
.filter((m): m is ChatMessage => m !== null);
|
|
114
|
+
return { messages, dropped };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface SaveSchedulerOptions {
|
|
118
|
+
/** The quiet period before a scheduled snapshot is written. This is policy
|
|
119
|
+
* and therefore REQUIRED — the kit ships no default debounce. */
|
|
120
|
+
delayMs: number;
|
|
121
|
+
/** The scheduler the seam runs on. Defaults to the global timers; inject
|
|
122
|
+
* your own for tests or a non-timer scheduler. */
|
|
123
|
+
setTimeout?: (fn: () => void, ms: number) => unknown;
|
|
124
|
+
clearTimeout?: (handle: unknown) => void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SaveScheduler<T> {
|
|
128
|
+
/** Remember `snapshot` as the latest state and (re)start the quiet-period
|
|
129
|
+
* timer. Call it on every state change; only the last snapshot inside the
|
|
130
|
+
* quiet period is written. */
|
|
131
|
+
schedule(snapshot: T): void;
|
|
132
|
+
/** Write the pending snapshot NOW and disarm the timer — the
|
|
133
|
+
* `beforeunload`/`visibilitychange` story, so a tab closed mid-stream still
|
|
134
|
+
* persists. No-op when nothing is pending: the last write already happened. */
|
|
135
|
+
flush(): void;
|
|
136
|
+
/** Drop the pending write unsaved (e.g. unmount-without-persist). */
|
|
137
|
+
cancel(): void;
|
|
138
|
+
pending(): boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The debounce/flush seam: a streaming reply lands a state update per token,
|
|
143
|
+
* and without this shape so would the consumer's storage backend. The kit owns
|
|
144
|
+
* only the timer dance; `save` (WHAT and WHERE, including its own try/catch
|
|
145
|
+
* policy for quota errors) and `delayMs` (HOW LONG) are the consumer's.
|
|
146
|
+
*/
|
|
147
|
+
export function createSaveScheduler<T>(save: (snapshot: T) => void, opts: SaveSchedulerOptions): SaveScheduler<T> {
|
|
148
|
+
const setTimer = opts.setTimeout ?? ((fn: () => void, ms: number) => setTimeout(fn, ms));
|
|
149
|
+
const clearTimer = opts.clearTimeout ?? ((handle: unknown) => clearTimeout(handle as ReturnType<typeof setTimeout>));
|
|
150
|
+
|
|
151
|
+
let latest: T | undefined;
|
|
152
|
+
let armed = false;
|
|
153
|
+
let handle: unknown;
|
|
154
|
+
|
|
155
|
+
const disarm = () => {
|
|
156
|
+
if (!armed) return;
|
|
157
|
+
clearTimer(handle);
|
|
158
|
+
armed = false;
|
|
159
|
+
};
|
|
160
|
+
const write = () => {
|
|
161
|
+
armed = false;
|
|
162
|
+
save(latest as T);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
schedule(snapshot) {
|
|
167
|
+
latest = snapshot;
|
|
168
|
+
disarm();
|
|
169
|
+
handle = setTimer(write, opts.delayMs);
|
|
170
|
+
armed = true;
|
|
171
|
+
},
|
|
172
|
+
flush() {
|
|
173
|
+
if (!armed) return;
|
|
174
|
+
disarm();
|
|
175
|
+
save(latest as T);
|
|
176
|
+
},
|
|
177
|
+
cancel: disarm,
|
|
178
|
+
pending: () => armed,
|
|
179
|
+
};
|
|
180
|
+
}
|
package/src/state/stream.ts
CHANGED
|
@@ -116,6 +116,56 @@ export interface AssistantStream {
|
|
|
116
116
|
* assignability already holds. Measurements and the trade: boundary point 2. */
|
|
117
117
|
addFile(attachment: AttachmentData): AssistantStream;
|
|
118
118
|
done(): void;
|
|
119
|
+
/** Settles the turn as FAILED and puts `reason` where the reader can find it.
|
|
120
|
+
*
|
|
121
|
+
* WHERE THE REASON LANDS, in order:
|
|
122
|
+
* 1. every tool part that has NOT produced a result flips to `output-error`
|
|
123
|
+
* with `errorText: reason`, so no panel spins forever. A tool ALREADY in
|
|
124
|
+
* `output-error` with its own non-empty `errorText` is left exactly as it
|
|
125
|
+
* is: "search index offline" is the answer to what went wrong and
|
|
126
|
+
* "Connection lost." is the generic outer symptom, so overwriting the
|
|
127
|
+
* specific with the generic loses the only actionable thing on the
|
|
128
|
+
* message. It still counts as carrying the failure. A tool in
|
|
129
|
+
* `output-error` with NO text does get filled in — an error panel with
|
|
130
|
+
* nothing in it says no more than a blank bubble does.
|
|
131
|
+
* 2. if no part was able to carry it, the reason is APPENDED as its own text
|
|
132
|
+
* part.
|
|
133
|
+
*
|
|
134
|
+
* Never both — a turn does not report the same failure twice.
|
|
135
|
+
*
|
|
136
|
+
* "Where the reader can FIND it" is deliberate, and rule 1 and rule 2 are not
|
|
137
|
+
* equally loud. Rule 2 is text in the thread: read without any interaction.
|
|
138
|
+
* Rule 1 is a tool panel, which renders COLLAPSED — the header shows an error
|
|
139
|
+
* icon and a badge, so the failure is unmissable, but the `errorText` itself
|
|
140
|
+
* is one click away. That is the right default for a failed tool inside an
|
|
141
|
+
* otherwise readable answer, and it is why rule 2 exists rather than always
|
|
142
|
+
* stamping the panel and calling it reported.
|
|
143
|
+
*
|
|
144
|
+
* Rule 2 is the whole point and it used to be missing. `abort` only ever did
|
|
145
|
+
* rule 1, so a TEXT-ONLY turn — every turn of a text-only support widget —
|
|
146
|
+
* had nothing to stamp: the string was discarded and a failed request
|
|
147
|
+
* rendered an EMPTY assistant bubble, while the consumer that passed a
|
|
148
|
+
* perfectly good sentence believed it had reported the failure. That is the
|
|
149
|
+
* repo's "decide loudly" rule broken on the one path where being quiet is
|
|
150
|
+
* worst, and it shipped because the discard is invisible from the call site.
|
|
151
|
+
*
|
|
152
|
+
* It is a NEW part, never a merge onto the trailing text: gluing "Connection
|
|
153
|
+
* lost." onto the model's half-finished sentence reads as the model saying
|
|
154
|
+
* it. Text that already streamed stays exactly where it is.
|
|
155
|
+
*
|
|
156
|
+
* The reason is TRIMMED, and `abort()` with no reason — or one that is empty
|
|
157
|
+
* or all whitespace — appends nothing: there is no reason to discard, and the
|
|
158
|
+
* kit will not invent copy the consumer did not write. Rule 1 still runs,
|
|
159
|
+
* with `errorText: undefined`. Whitespace is not a pedantic case here: the
|
|
160
|
+
* scaffold hands over `err.message`, and an `Error` is free to carry `''`,
|
|
161
|
+
* which would otherwise render an INVISIBLE text part — a blank bubble that
|
|
162
|
+
* also claims to have said something.
|
|
163
|
+
*
|
|
164
|
+
* Settled is settled: after `done()` or a first `abort()`, this is a no-op
|
|
165
|
+
* like every other mutator, so the reason cannot be appended twice.
|
|
166
|
+
*
|
|
167
|
+
* The reason is CONSUMER-facing text and is rendered as markdown like any
|
|
168
|
+
* other text part. Pass a sentence a visitor can read, not a stack trace. */
|
|
119
169
|
abort(reason?: string): void;
|
|
120
170
|
}
|
|
121
171
|
|
|
@@ -151,10 +201,27 @@ export function createAssistantStream(
|
|
|
151
201
|
addFile(attachment) { mutate((p) => [...p, { type: 'file', attachment }]); return stream; },
|
|
152
202
|
done() { settled = true; },
|
|
153
203
|
abort(reason) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
204
|
+
// Whitespace-only is the same nothing as empty. `err.message` on a thrown
|
|
205
|
+
// Error is free to be either, and appending it would render an INVISIBLE
|
|
206
|
+
// text part -- a blank bubble that also claims to have said something.
|
|
207
|
+
const text = reason?.trim() || undefined;
|
|
208
|
+
mutate((p) => {
|
|
209
|
+
let carried = false;
|
|
210
|
+
const next = p.map((part) => {
|
|
211
|
+
if (part.type !== 'tool' || part.tool.state === 'output-available') return part;
|
|
212
|
+
// Already showing its own, more specific failure: keep it, and count
|
|
213
|
+
// it as carrying. See the note above `abort`.
|
|
214
|
+
if (part.tool.state === 'output-error' && part.tool.errorText?.trim()) {
|
|
215
|
+
carried = true;
|
|
216
|
+
return part;
|
|
217
|
+
}
|
|
218
|
+
carried = true;
|
|
219
|
+
return { ...part, tool: { ...part.tool, state: 'output-error' as const, errorText: text } };
|
|
220
|
+
});
|
|
221
|
+
// The reason has to land SOMEWHERE. See the note above `abort`.
|
|
222
|
+
if (carried || !text) return next;
|
|
223
|
+
return [...next, { type: 'text', text }];
|
|
224
|
+
});
|
|
158
225
|
settled = true;
|
|
159
226
|
},
|
|
160
227
|
};
|