@kitn.ai/ui 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-tooling/construct/public.d.ts +15 -0
  3. package/dist/agent-tooling/construct/schema.d.ts +111 -0
  4. package/dist/construct-cli.es.js +3 -3
  5. package/dist/construct.d.ts +5 -0
  6. package/dist/construct.js +327 -0
  7. package/dist/core-C8fzo39E.js +1 -12
  8. package/dist/engine-javascript-C1x7zo1_.js +1 -141
  9. package/dist/mcp.es.js +1 -1
  10. package/package.json +16 -16
  11. package/bin/route.test.js +0 -58
  12. package/src/agent-tooling/README.md +0 -192
  13. package/src/agent-tooling/archetypes.ts +0 -205
  14. package/src/agent-tooling/catalog/README.md +0 -676
  15. package/src/agent-tooling/catalog/catalog-types.ts +0 -231
  16. package/src/agent-tooling/catalog/fabrications.ts +0 -96
  17. package/src/agent-tooling/catalog/invariants.ts +0 -284
  18. package/src/agent-tooling/catalog/labs-titles.ts +0 -114
  19. package/src/agent-tooling/catalog/scenarios.ts +0 -87
  20. package/src/agent-tooling/catalog/surfaces.ts +0 -347
  21. package/src/agent-tooling/construct/cli-entry.ts +0 -8
  22. package/src/agent-tooling/construct/cli.ts +0 -147
  23. package/src/agent-tooling/construct/codegen.ts +0 -1635
  24. package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
  25. package/src/agent-tooling/construct/dev.ts +0 -148
  26. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
  27. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
  28. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
  29. package/src/agent-tooling/construct/schema.ts +0 -452
  30. package/src/agent-tooling/integrations/anthropic.ts +0 -442
  31. package/src/agent-tooling/integrations/cloudflare.ts +0 -218
  32. package/src/agent-tooling/integrations/langgraph.ts +0 -133
  33. package/src/agent-tooling/integrations/mastra.ts +0 -182
  34. package/src/agent-tooling/integrations/mock.ts +0 -148
  35. package/src/agent-tooling/integrations/ollama.ts +0 -97
  36. package/src/agent-tooling/integrations/openai.ts +0 -95
  37. package/src/agent-tooling/integrations/openrouter.ts +0 -82
  38. package/src/agent-tooling/integrations/pi.ts +0 -176
  39. package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
  40. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
  41. package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
  42. package/src/agent-tooling/mcp/manifest.ts +0 -422
  43. package/src/agent-tooling/mcp/server.ts +0 -116
  44. package/src/agent-tooling/mcp/stdio.ts +0 -18
  45. package/src/agent-tooling/mcp/tools/construct.ts +0 -130
  46. package/src/agent-tooling/mcp/tools/debug.ts +0 -621
  47. package/src/agent-tooling/mcp/tools/reference.ts +0 -980
  48. package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
  49. package/src/agent-tooling/mcp/tools/theme.ts +0 -456
  50. package/src/agent-tooling/mcp/tools/types.ts +0 -14
  51. package/src/agent-tooling/mcp/validate-args.ts +0 -141
  52. package/src/agent-tooling/recipes/composed-thread.ts +0 -714
  53. package/src/agent-tooling/recipes/index.ts +0 -22
  54. package/src/agent-tooling/recipes/types.ts +0 -32
  55. package/src/agent-tooling/registry.ts +0 -132
  56. package/src/agent-tooling/route-emit.ts +0 -305
  57. package/src/agent-tooling/types.ts +0 -390
  58. package/src/components/artifact-card.tsx +0 -120
  59. package/src/components/artifact.tsx +0 -914
  60. package/src/components/attachment-types.ts +0 -66
  61. package/src/components/attachments.tsx +0 -536
  62. package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
  63. package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
  64. package/src/components/audio-visualizer/fit-scale.ts +0 -112
  65. package/src/components/audio-visualizer/index.tsx +0 -528
  66. package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
  67. package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
  68. package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
  69. package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
  70. package/src/components/audio-visualizer/sizes.ts +0 -54
  71. package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
  72. package/src/components/audio-visualizer/variant-bar.tsx +0 -175
  73. package/src/components/audio-visualizer/variant-custom.tsx +0 -344
  74. package/src/components/audio-visualizer/variant-grid.tsx +0 -175
  75. package/src/components/audio-visualizer/variant-radial.tsx +0 -163
  76. package/src/components/audio-visualizer/variant-wave.tsx +0 -130
  77. package/src/components/audio-visualizer/wave.glsl.ts +0 -130
  78. package/src/components/card-fallback.tsx +0 -60
  79. package/src/components/card-renderer.tsx +0 -220
  80. package/src/components/card.tsx +0 -212
  81. package/src/components/chain-of-thought.tsx +0 -348
  82. package/src/components/chat-container.tsx +0 -89
  83. package/src/components/chat-scope-picker.tsx +0 -57
  84. package/src/components/chat-thread.tsx +0 -1014
  85. package/src/components/checkpoint.tsx +0 -88
  86. package/src/components/choice-card.tsx +0 -743
  87. package/src/components/coachmark.tsx +0 -249
  88. package/src/components/code-block.tsx +0 -181
  89. package/src/components/composer-dom.ts +0 -159
  90. package/src/components/composer-highlight.ts +0 -242
  91. package/src/components/composer-history.ts +0 -82
  92. package/src/components/composer.tsx +0 -1075
  93. package/src/components/confirm-card.tsx +0 -402
  94. package/src/components/context.tsx +0 -377
  95. package/src/components/conversation-item.tsx +0 -241
  96. package/src/components/conversation-list.tsx +0 -429
  97. package/src/components/conversation-panel.tsx +0 -118
  98. package/src/components/dismissed-stub.tsx +0 -80
  99. package/src/components/embed.tsx +0 -200
  100. package/src/components/empty.tsx +0 -177
  101. package/src/components/feedback-bar.tsx +0 -166
  102. package/src/components/file-tree.tsx +0 -530
  103. package/src/components/file-upload.tsx +0 -167
  104. package/src/components/form-widgets.tsx +0 -525
  105. package/src/components/form.tsx +0 -1296
  106. package/src/components/home-panel.tsx +0 -159
  107. package/src/components/image.tsx +0 -65
  108. package/src/components/link-preview.tsx +0 -195
  109. package/src/components/loader.tsx +0 -367
  110. package/src/components/markdown.tsx +0 -162
  111. package/src/components/message-skills.tsx +0 -40
  112. package/src/components/message.tsx +0 -765
  113. package/src/components/model-switcher.tsx +0 -132
  114. package/src/components/prompt-input.tsx +0 -188
  115. package/src/components/prompt-suggestion.tsx +0 -150
  116. package/src/components/reasoning.tsx +0 -293
  117. package/src/components/response-compare-types.ts +0 -152
  118. package/src/components/response-compare.tsx +0 -455
  119. package/src/components/response-stream.tsx +0 -109
  120. package/src/components/screen.tsx +0 -181
  121. package/src/components/scroll-button.tsx +0 -93
  122. package/src/components/source.tsx +0 -192
  123. package/src/components/tasks-card.tsx +0 -801
  124. package/src/components/text-shimmer.tsx +0 -37
  125. package/src/components/thinking-bar.tsx +0 -50
  126. package/src/components/thread.tsx +0 -228
  127. package/src/components/toast.tsx +0 -503
  128. package/src/components/tool-classify.ts +0 -26
  129. package/src/components/tool-types.ts +0 -49
  130. package/src/components/tool.tsx +0 -190
  131. package/src/components/use-card-resolution.ts +0 -44
  132. package/src/components/voice-input.tsx +0 -222
  133. package/src/components/voice-output.tsx +0 -242
  134. package/src/components/widget-tab-bar.tsx +0 -73
  135. package/src/components/workspace-shell.tsx +0 -285
  136. package/src/diagnostics/hook.ts +0 -352
  137. package/src/diagnostics/index.ts +0 -82
  138. package/src/diagnostics/report-request.ts +0 -296
  139. package/src/elements/agent-card.tsx +0 -67
  140. package/src/elements/artifact.tsx +0 -194
  141. package/src/elements/attachments.tsx +0 -140
  142. package/src/elements/audio-visualizer.tsx +0 -227
  143. package/src/elements/autoloader.ts +0 -89
  144. package/src/elements/avatar.tsx +0 -33
  145. package/src/elements/badge.tsx +0 -32
  146. package/src/elements/button.tsx +0 -149
  147. package/src/elements/card.tsx +0 -133
  148. package/src/elements/cards.tsx +0 -266
  149. package/src/elements/chain-of-thought.tsx +0 -141
  150. package/src/elements/chat-scope-picker.tsx +0 -65
  151. package/src/elements/chat-types.ts +0 -102
  152. package/src/elements/chat-workspace.tsx +0 -188
  153. package/src/elements/chat.tsx +0 -304
  154. package/src/elements/checkbox-group.tsx +0 -164
  155. package/src/elements/checkbox.tsx +0 -134
  156. package/src/elements/checkpoint.tsx +0 -47
  157. package/src/elements/choice.tsx +0 -100
  158. package/src/elements/coachmark.tsx +0 -90
  159. package/src/elements/code-block.tsx +0 -60
  160. package/src/elements/command.tsx +0 -197
  161. package/src/elements/compare.tsx +0 -111
  162. package/src/elements/compiled.css +0 -2
  163. package/src/elements/composer.tsx +0 -162
  164. package/src/elements/confirm-card.tsx +0 -76
  165. package/src/elements/context-meter.tsx +0 -107
  166. package/src/elements/conversation-item.tsx +0 -120
  167. package/src/elements/conversation-list.tsx +0 -203
  168. package/src/elements/css.ts +0 -5
  169. package/src/elements/default-input.tsx +0 -392
  170. package/src/elements/define-entry.ts +0 -12
  171. package/src/elements/define.tsx +0 -563
  172. package/src/elements/diagnostic-events.ts +0 -114
  173. package/src/elements/dialog.tsx +0 -145
  174. package/src/elements/disclosure.ts +0 -95
  175. package/src/elements/dock.tsx +0 -215
  176. package/src/elements/dropdown.tsx +0 -123
  177. package/src/elements/editable-label.tsx +0 -140
  178. package/src/elements/element-data-types.ts +0 -152
  179. package/src/elements/element-diagnostics.ts +0 -392
  180. package/src/elements/element-manifest.json +0 -354
  181. package/src/elements/element-nonscalar.json +0 -187
  182. package/src/elements/element-types.d.ts +0 -4353
  183. package/src/elements/embed.tsx +0 -35
  184. package/src/elements/empty.tsx +0 -29
  185. package/src/elements/feedback-bar.tsx +0 -66
  186. package/src/elements/file-tree.tsx +0 -62
  187. package/src/elements/file-upload.tsx +0 -44
  188. package/src/elements/form.tsx +0 -94
  189. package/src/elements/hover-card.tsx +0 -80
  190. package/src/elements/icon.tsx +0 -41
  191. package/src/elements/image.tsx +0 -32
  192. package/src/elements/input.tsx +0 -492
  193. package/src/elements/kbd.tsx +0 -45
  194. package/src/elements/link-preview.tsx +0 -34
  195. package/src/elements/loader.tsx +0 -25
  196. package/src/elements/markdown.tsx +0 -38
  197. package/src/elements/menu.tsx +0 -241
  198. package/src/elements/message-skills.tsx +0 -83
  199. package/src/elements/message.tsx +0 -361
  200. package/src/elements/model-switcher.tsx +0 -121
  201. package/src/elements/nav.tsx +0 -87
  202. package/src/elements/notice.tsx +0 -53
  203. package/src/elements/pane-grid.tsx +0 -111
  204. package/src/elements/pane-group.tsx +0 -119
  205. package/src/elements/pane.tsx +0 -129
  206. package/src/elements/popover.tsx +0 -81
  207. package/src/elements/progress-bar.tsx +0 -39
  208. package/src/elements/prompt-dock.tsx +0 -82
  209. package/src/elements/prompt-input.tsx +0 -240
  210. package/src/elements/prompt-suggestions.tsx +0 -113
  211. package/src/elements/radio-group.tsx +0 -129
  212. package/src/elements/reasoning.tsx +0 -71
  213. package/src/elements/register-impl.ts +0 -148
  214. package/src/elements/register.ts +0 -76
  215. package/src/elements/remote.tsx +0 -225
  216. package/src/elements/resizable.globals.d.ts +0 -36
  217. package/src/elements/resizable.tsx +0 -704
  218. package/src/elements/response-stream.tsx +0 -40
  219. package/src/elements/screen.tsx +0 -90
  220. package/src/elements/scroll-area.tsx +0 -33
  221. package/src/elements/scroll-button.tsx +0 -179
  222. package/src/elements/search.tsx +0 -185
  223. package/src/elements/segmented.tsx +0 -129
  224. package/src/elements/select.tsx +0 -165
  225. package/src/elements/separator.tsx +0 -36
  226. package/src/elements/setting-item.tsx +0 -50
  227. package/src/elements/settings-group.tsx +0 -37
  228. package/src/elements/skeleton.tsx +0 -45
  229. package/src/elements/slider.tsx +0 -171
  230. package/src/elements/slot-text.ts +0 -73
  231. package/src/elements/slots.ts +0 -787
  232. package/src/elements/source.tsx +0 -126
  233. package/src/elements/status.tsx +0 -42
  234. package/src/elements/styles.css +0 -328
  235. package/src/elements/switch.tsx +0 -118
  236. package/src/elements/tabs.tsx +0 -95
  237. package/src/elements/tasks.tsx +0 -103
  238. package/src/elements/text-shimmer.tsx +0 -28
  239. package/src/elements/thinking-bar.tsx +0 -43
  240. package/src/elements/thread.tsx +0 -139
  241. package/src/elements/toast.tsx +0 -106
  242. package/src/elements/tool.tsx +0 -78
  243. package/src/elements/tooltip.tsx +0 -85
  244. package/src/elements/validate-messages.ts +0 -76
  245. package/src/elements/voice-input.tsx +0 -100
  246. package/src/elements/voice-output.tsx +0 -82
  247. package/src/index.ts +0 -430
  248. package/src/primitives/audio-bands.ts +0 -174
  249. package/src/primitives/card-component-types.ts +0 -63
  250. package/src/primitives/card-contract.ts +0 -80
  251. package/src/primitives/card-data-types.ts +0 -361
  252. package/src/primitives/card-host.tsx +0 -37
  253. package/src/primitives/card-recovery.ts +0 -160
  254. package/src/primitives/card-registry.tsx +0 -78
  255. package/src/primitives/card-resolution.ts +0 -36
  256. package/src/primitives/card-routing.ts +0 -85
  257. package/src/primitives/card-schemas/artifact.schema.json +0 -92
  258. package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
  259. package/src/primitives/card-schemas/card-event.schema.json +0 -12
  260. package/src/primitives/card-schemas/choice.schema.json +0 -75
  261. package/src/primitives/card-schemas/confirm.schema.json +0 -65
  262. package/src/primitives/card-schemas/embed.schema.json +0 -63
  263. package/src/primitives/card-schemas/form.result.schema.json +0 -7
  264. package/src/primitives/card-schemas/form.schema.json +0 -53
  265. package/src/primitives/card-schemas/link.schema.json +0 -54
  266. package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
  267. package/src/primitives/card-schemas/tasks.schema.json +0 -83
  268. package/src/primitives/card-tags.ts +0 -61
  269. package/src/primitives/card-validate-cards.ts +0 -255
  270. package/src/primitives/card-validate-schemas.ts +0 -58
  271. package/src/primitives/card-validate.ts +0 -131
  272. package/src/primitives/chat-config.tsx +0 -103
  273. package/src/primitives/composer-model.ts +0 -49
  274. package/src/primitives/composer-triggers.ts +0 -35
  275. package/src/primitives/controllable.ts +0 -21
  276. package/src/primitives/conversation-store.ts +0 -248
  277. package/src/primitives/create-kai-chat.ts +0 -66
  278. package/src/primitives/create-tween.ts +0 -266
  279. package/src/primitives/embed-providers.ts +0 -254
  280. package/src/primitives/field-mask.ts +0 -256
  281. package/src/primitives/field-semantics.ts +0 -115
  282. package/src/primitives/highlighter.ts +0 -157
  283. package/src/primitives/input-mask.ts +0 -853
  284. package/src/primitives/link-preview.ts +0 -87
  285. package/src/primitives/message-feedback.ts +0 -124
  286. package/src/primitives/pdf-preview.ts +0 -121
  287. package/src/primitives/toast-store.ts +0 -338
  288. package/src/primitives/url-scheme-policy.ts +0 -70
  289. package/src/primitives/use-audio-analysis.ts +0 -340
  290. package/src/primitives/use-auto-resize.ts +0 -31
  291. package/src/primitives/use-resize-observer.ts +0 -45
  292. package/src/primitives/use-sequencer.ts +0 -72
  293. package/src/primitives/use-speech-recognition.ts +0 -146
  294. package/src/primitives/use-stick-to-bottom.ts +0 -75
  295. package/src/primitives/use-text-stream.ts +0 -143
  296. package/src/primitives/use-voice-recorder.ts +0 -62
  297. package/src/primitives/visualizer-sequences.ts +0 -350
  298. package/src/remote/host-embed.ts +0 -345
  299. package/src/remote/index.ts +0 -2
  300. package/src/remote/origin.ts +0 -30
  301. package/src/remote/provider-runtime.ts +0 -262
  302. package/src/remote/provider.ts +0 -2
  303. package/src/remote/validate.ts +0 -22
  304. package/src/remote/version.ts +0 -12
  305. package/src/remote/wire.ts +0 -48
  306. package/src/schemas/from-tool-call.ts +0 -151
  307. package/src/schemas/index.ts +0 -341
  308. package/src/schemas/provider-subsets.ts +0 -538
  309. package/src/schemas/registry.ts +0 -355
  310. package/src/schemas/tool-defs.ts +0 -793
  311. package/src/solid.ts +0 -173
  312. package/src/state/index.ts +0 -44
  313. package/src/state/messages.ts +0 -53
  314. package/src/state/mock.ts +0 -297
  315. package/src/state/parts.ts +0 -295
  316. package/src/state/persistence.ts +0 -180
  317. package/src/state/stream.ts +0 -246
  318. package/src/state/suggestions.ts +0 -9
  319. package/src/state/threads.ts +0 -142
  320. package/src/test-utils/fake-clock.ts +0 -88
  321. package/src/types.ts +0 -95
  322. package/src/ui/action-icons.ts +0 -53
  323. package/src/ui/agent-card.tsx +0 -218
  324. package/src/ui/avatar.tsx +0 -23
  325. package/src/ui/badge.tsx +0 -21
  326. package/src/ui/button.tsx +0 -49
  327. package/src/ui/card.tsx +0 -245
  328. package/src/ui/checkbox-group.tsx +0 -153
  329. package/src/ui/checkbox.tsx +0 -63
  330. package/src/ui/collapsible.tsx +0 -163
  331. package/src/ui/command.tsx +0 -103
  332. package/src/ui/dialog.tsx +0 -223
  333. package/src/ui/dock.tsx +0 -630
  334. package/src/ui/dropdown.tsx +0 -636
  335. package/src/ui/editable-label.tsx +0 -112
  336. package/src/ui/hover-card.tsx +0 -345
  337. package/src/ui/icon.tsx +0 -167
  338. package/src/ui/input.tsx +0 -391
  339. package/src/ui/kbd.tsx +0 -118
  340. package/src/ui/nav.tsx +0 -282
  341. package/src/ui/notice.tsx +0 -84
  342. package/src/ui/overlay.tsx +0 -277
  343. package/src/ui/pane-grid.tsx +0 -117
  344. package/src/ui/pane-group.tsx +0 -291
  345. package/src/ui/pane.tsx +0 -193
  346. package/src/ui/popover.tsx +0 -106
  347. package/src/ui/progress-bar.tsx +0 -83
  348. package/src/ui/prompt-dock.tsx +0 -145
  349. package/src/ui/radio.tsx +0 -150
  350. package/src/ui/resizable.tsx +0 -693
  351. package/src/ui/scroll-area.tsx +0 -26
  352. package/src/ui/segmented.tsx +0 -109
  353. package/src/ui/select.tsx +0 -168
  354. package/src/ui/separator.tsx +0 -10
  355. package/src/ui/settings-group.tsx +0 -67
  356. package/src/ui/skeleton.tsx +0 -73
  357. package/src/ui/slider.tsx +0 -178
  358. package/src/ui/status.tsx +0 -48
  359. package/src/ui/switch.tsx +0 -119
  360. package/src/ui/tabs.tsx +0 -168
  361. package/src/ui/textarea.tsx +0 -21
  362. package/src/ui/tooltip.tsx +0 -118
  363. package/src/utils/cn.ts +0 -30
  364. package/src/wire/chunk.ts +0 -248
  365. package/src/wire/consume.ts +0 -599
  366. package/src/wire/diagnostics.ts +0 -727
  367. package/src/wire/encode-probe.ts +0 -214
  368. package/src/wire/encode.ts +0 -947
  369. package/src/wire/files.ts +0 -342
  370. package/src/wire/formats/anthropic.ts +0 -274
  371. package/src/wire/formats/openai.ts +0 -228
  372. package/src/wire/index.ts +0 -95
  373. package/src/wire/media-types.ts +0 -344
  374. package/src/wire/read.ts +0 -396
  375. package/src/wire/sink-helpers.ts +0 -50
  376. package/src/wire/sse.ts +0 -153
@@ -1,765 +0,0 @@
1
- import { type JSX, For, Index, Switch, Match, createMemo, createSignal, splitProps, Show } from "solid-js";
2
- import { Tooltip } from "../ui/tooltip";
3
- import { Copy, Check } from "lucide-solid";
4
- import { cn } from "../utils/cn";
5
- import { Markdown } from "./markdown";
6
- import { Button } from "../ui/button";
7
- import { actionIcon, BUILTIN_ACTION_LABEL } from "../ui/action-icons";
8
- import type { ChatMessageAction, CustomAction, FeedbackVote, MessagePart, MessageSource } from "../elements/chat-types";
9
- import { useChatConfig, textClass } from "../primitives/chat-config";
10
- import { Reasoning, ReasoningTrigger, ReasoningContent } from "./reasoning";
11
- import { Loader } from "./loader";
12
- import { Tool } from "./tool";
13
- import {
14
- Attachments,
15
- Attachment,
16
- AttachmentPreview,
17
- AttachmentInfo,
18
- AttachmentHoverCard,
19
- AttachmentHoverCardTrigger,
20
- AttachmentHoverCardContent,
21
- getAttachmentLabel,
22
- getMediaCategory,
23
- } from "./attachments";
24
- import { Source, SourceTrigger, SourceContent, SourceList } from "./source";
25
- import { CardRenderer, type CardSchemaMap } from "./card-renderer";
26
- import type { CardComponentMap } from "../primitives/card-registry";
27
-
28
- // --- Message ---
29
-
30
- /** Who is speaking in a message row. This is the SEMANTIC role of the message,
31
- * not an ARIA role — see `MessageProps['role']`. */
32
- export type MessageRole = 'user' | 'assistant' | 'system';
33
-
34
- /** The accessible name given to a row that declares a speaker. `role="article"`
35
- * supports naming; the bare `<div>` a role-less `<Message>` renders does not,
36
- * so the label is only attached alongside that role. */
37
- const MESSAGE_ROLE_LABEL: Record<MessageRole, string> = {
38
- user: 'User message',
39
- assistant: 'Assistant message',
40
- system: 'System message',
41
- };
42
-
43
- export interface MessageProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'role'> {
44
- children: JSX.Element;
45
- /** Who is speaking: `'user'`, `'assistant'` or `'system'`.
46
- *
47
- * This SHADOWS the inherited ARIA `role` attribute on purpose, and is the
48
- * reason `JSX.HTMLAttributes` is `Omit`ted above. Before this existed, `role`
49
- * resolved to ARIA and was spread straight onto the row, so `<Message
50
- * role="user">` shipped `role="user"` on a div — not a valid ARIA role, and a
51
- * critical axe `aria-roles` violation ("Role must be one of the valid ARIA
52
- * roles: user") on every message that used the prop. Chromium discards the
53
- * unknown token and computes `generic`, so the damage was a failed a11y audit
54
- * and a row with no accessible role or name at all, not a mis-announced one.
55
- *
56
- * It is now consumed here and never reaches the DOM. What the row gets
57
- * instead is a VALID ARIA role that can carry a name — `role="article"` plus
58
- * an `aria-label` naming the speaker — and `data-role` for styling and
59
- * querying. Omit the prop and the row stays an unlabelled `<div>`, exactly as
60
- * before, so the many role-less call sites are untouched.
61
- *
62
- * The trade-off: the row's ARIA role can no longer be set through this prop.
63
- * Pass `aria-label` (or any other ARIA attribute) to override the default
64
- * naming — the spread below still wins over what this computes. */
65
- role?: MessageRole;
66
- }
67
-
68
- function Message(props: MessageProps) {
69
- const [local, rest] = splitProps(props, ["children", "class", "role"]);
70
- return (
71
- <div
72
- part="row"
73
- class={cn("flex items-start gap-3", local.class)}
74
- data-role={local.role}
75
- role={local.role ? 'article' : undefined}
76
- aria-label={local.role ? MESSAGE_ROLE_LABEL[local.role] : undefined}
77
- {...rest}
78
- >
79
- {local.children}
80
- </div>
81
- );
82
- }
83
-
84
- // --- MessageAvatar ---
85
-
86
- export interface MessageAvatarProps {
87
- /** Avatar image URL. Optional: with no `src` the component renders `fallback`
88
- * (initials), which is the whole point of having a fallback — requiring `src`
89
- * forced `<MessageAvatar src="" …/>` at every initials-only call site. */
90
- src?: string;
91
- /** Alt text for the image. Only meaningful alongside `src`; defaults to `''`
92
- * so an avatar stays decorative rather than announcing a filename. */
93
- alt?: string;
94
- fallback?: string;
95
- class?: string;
96
- }
97
-
98
- function MessageAvatar(props: MessageAvatarProps) {
99
- return (
100
- <div
101
- part="avatar"
102
- class={cn("h-8 w-8 shrink-0 overflow-hidden rounded-full", props.class)}
103
- >
104
- <Show
105
- when={props.src}
106
- fallback={
107
- <Show when={props.fallback}>
108
- <div class="flex h-full w-full items-center justify-center bg-muted text-xs font-medium text-muted-foreground">
109
- {props.fallback}
110
- </div>
111
- </Show>
112
- }
113
- >
114
- <img
115
- src={props.src}
116
- alt={props.alt ?? ''}
117
- class="h-full w-full object-cover"
118
- />
119
- </Show>
120
- </div>
121
- );
122
- }
123
-
124
- // --- MessageContent ---
125
-
126
- export interface MessageContentProps extends JSX.HTMLAttributes<HTMLDivElement> {
127
- children: JSX.Element | string;
128
- markdown?: boolean;
129
- /** `::part` name(s) exposed on the content node. The body passes
130
- * `"bubble content"` so consumers can target either the rounded bubble or the
131
- * text region from outside the shadow boundary. */
132
- part?: string;
133
- }
134
-
135
- function MessageContent(props: MessageContentProps) {
136
- const [local, rest] = splitProps(props, ["children", "markdown", "class", "part"]);
137
- const config = useChatConfig();
138
- const classNames = () =>
139
- cn(
140
- "min-w-0 rounded-lg p-2 text-foreground bg-secondary max-w-none break-words whitespace-normal",
141
- textClass(config.proseSize()),
142
- local.class,
143
- );
144
-
145
- return (
146
- <Show
147
- when={local.markdown}
148
- fallback={
149
- <div part={local.part} class={classNames()} {...rest}>
150
- {local.children}
151
- </div>
152
- }
153
- >
154
- <Markdown part={local.part} content={local.children as string} class={classNames()} />
155
- </Show>
156
- );
157
- }
158
-
159
- // --- MessageActions ---
160
-
161
- export interface MessageActionsProps extends JSX.HTMLAttributes<HTMLDivElement> {
162
- children: JSX.Element;
163
- }
164
-
165
- function MessageActions(props: MessageActionsProps) {
166
- const [local, rest] = splitProps(props, ["children", "class"]);
167
- return (
168
- <div
169
- class={cn(
170
- "flex items-center gap-0.5 mt-0.5",
171
- local.class,
172
- )}
173
- {...rest}
174
- >
175
- {local.children}
176
- </div>
177
- );
178
- }
179
-
180
- // --- MessageActionBar ---
181
-
182
- export interface MessageActionBarProps {
183
- /** Built-in action names and/or custom action descriptors, in order. */
184
- actions: (ChatMessageAction | CustomAction)[];
185
- /** `'always'` (default) keeps the bar visible; `'hover'` reveals it on
186
- * parent `.group` hover. */
187
- reveal?: 'always' | 'hover';
188
- /** Fired with the built-in name or the custom action id when a button is clicked. */
189
- onAction: (id: string) => void;
190
- /** The active feedback vote. When `'like'`/`'dislike'` is set, that button is
191
- * marked pressed (filled) and the OTHER vote button animates out. `undefined`
192
- * shows both. Pure/prop-driven so the bar survives re-renders. */
193
- activeFeedback?: FeedbackVote;
194
- /** When true, the `copy` button shows its emerald check icon instead of the
195
- * copy glyph (cleared by the owner after ~2s). */
196
- copied?: boolean;
197
- class?: string;
198
- }
199
-
200
- /** Normalize a bar entry (string built-in or custom object) to a uniform shape. */
201
- function normalizeAction(a: ChatMessageAction | CustomAction) {
202
- if (typeof a === 'string') {
203
- return { id: a, label: BUILTIN_ACTION_LABEL[a], Icon: actionIcon(a) };
204
- }
205
- return { id: a.id, label: a.label, Icon: actionIcon(a.icon) };
206
- }
207
-
208
- /** Is this entry one of the two feedback votes? */
209
- function feedbackVoteOf(a: ChatMessageAction | CustomAction): FeedbackVote | undefined {
210
- return a === 'like' || a === 'dislike' ? a : undefined;
211
- }
212
-
213
- /**
214
- * The shared message action toolbar. Renders one ghost icon button per entry —
215
- * built-in names pull their label+icon from the curated registry; custom
216
- * descriptors use their `label` plus `actionIcon(icon)` (label-only when the
217
- * icon is unknown or absent). `reveal="hover"` makes the bar fade in on the
218
- * parent `.group`'s hover.
219
- *
220
- * Pure/prop-driven: feedback (`activeFeedback`) and copy (`copied`) state are
221
- * owned by the parent facade and passed in — the bar holds no internal signals,
222
- * so it survives the new-array-per-chunk re-renders of a streaming thread. With
223
- * a vote active, the chosen `like`/`dislike` button is marked `aria-pressed` +
224
- * filled and the other vote button collapses its width (sliding the active thumb
225
- * into its place) via a 0fr↔1fr grid transition. The
226
- * `copy` button swaps to an emerald check while `copied`.
227
- */
228
- function MessageActionBar(props: MessageActionBarProps) {
229
- return (
230
- <MessageActions
231
- part="actions"
232
- class={cn(
233
- 'mt-1 flex gap-0',
234
- props.reveal === 'hover' && 'opacity-0 transition-opacity group-hover:opacity-100',
235
- props.class,
236
- )}
237
- >
238
- <For each={props.actions}>
239
- {(a) => {
240
- const item = normalizeAction(a);
241
- const tooltipText = () => (typeof a !== 'string' && a.tooltip) ? a.tooltip : item.label;
242
- const vote = feedbackVoteOf(a);
243
- // A vote button is the active one when it matches the resolved vote.
244
- const isActiveVote = () => vote !== undefined && props.activeFeedback === vote;
245
- // The COPY button reflects the copied check.
246
- const isCopy = item.id === 'copy';
247
- const showCheck = () => isCopy && props.copied === true;
248
-
249
- // Factory (not a shared node): each Show branch gets its own Button so
250
- // the eager DOM node is never referenced from two places.
251
- const button = () => (
252
- <Button
253
- variant="ghost"
254
- size="icon-sm"
255
- class={cn('rounded-full', isActiveVote() && 'text-primary')}
256
- data-action={item.id}
257
- aria-label={showCheck() ? 'Copied' : item.label}
258
- aria-pressed={vote !== undefined ? isActiveVote() : undefined}
259
- onClick={() => props.onAction(item.id)}
260
- >
261
- <Show
262
- when={showCheck()}
263
- fallback={
264
- <Show when={item.Icon} fallback={<span class="px-1 text-xs">{item.label}</span>}>
265
- {(Icon) => {
266
- const I = Icon();
267
- return <I class={cn('size-3.5', isActiveVote() && 'fill-current')} />;
268
- }}
269
- </Show>
270
- }
271
- >
272
- <Check class="size-3.5 text-emerald-400" />
273
- </Show>
274
- </Button>
275
- );
276
- // Icon-only buttons get a tooltip; label-only buttons (text already
277
- // visible) don't.
278
- const rendered = () => (
279
- <Show when={item.Icon} fallback={button()}>
280
- <Tooltip content={tooltipText()}>{button()}</Tooltip>
281
- </Show>
282
- );
283
-
284
- // When the OTHER vote is active, this vote button collapses — its WIDTH
285
- // animates to zero via a 0fr↔1fr grid so the remaining thumb slides into
286
- // its place, while it fades out; it slides + fades back on un-vote. (Kept
287
- // mounted-but-collapsed, not unmounted, so the sibling can slide.) No
288
- // vote active → both shown; non-vote entries always render.
289
- return (
290
- <Show when={vote !== undefined} fallback={rendered()}>
291
- {(() => {
292
- const show = () => props.activeFeedback === undefined || props.activeFeedback === vote;
293
- return (
294
- <span
295
- data-feedback-collapsed={show() ? undefined : ''}
296
- class={cn(
297
- 'grid transition-[grid-template-columns,opacity] duration-300 ease-out',
298
- show() ? 'grid-cols-[1fr] opacity-100' : 'grid-cols-[0fr] opacity-0',
299
- )}
300
- >
301
- <span class="min-w-0 overflow-hidden">{rendered()}</span>
302
- </span>
303
- );
304
- })()}
305
- </Show>
306
- );
307
- }}
308
- </For>
309
- </MessageActions>
310
- );
311
- }
312
-
313
- // --- MessageBody ---
314
-
315
- export interface MessageBodyProps {
316
- /** The message's ordered parts (text, reasoning, tool, card, source, file),
317
- * rendered in a single pass in the order they appear. A run of consecutive
318
- * `source` parts renders as ONE citation row (`part="citations"`), placed
319
- * OUTSIDE the message bubble so a citation is never confused with a link the
320
- * model typed into its prose. */
321
- parts: MessagePart[];
322
- /** Add/override card type -> component entries, forwarded to `CardRenderer`
323
- * for `card` parts. */
324
- cardTypes?: CardComponentMap;
325
- /** JSON Schemas for the card types this app renders, keyed by envelope type,
326
- * forwarded to `CardRenderer` for `card` parts. The companion of `cardTypes`:
327
- * that says what DRAWS a card, this says what a VALID one looks like.
328
- * `createCardRegistry(...).validationSchemas` is exactly this shape. Without it
329
- * the kit checks its own seven built-ins and leaves your own card type
330
- * unvalidated. A schema here WINS over a built-in of the same name. */
331
- cardSchemas?: CardSchemaMap;
332
- /** The custom-element host node to emit card events off when no `CardProvider`
333
- * is above this body, forwarded to `CardRenderer` as `hostElement`. The
334
- * `<kai-chat>`/`<kai-message>`/`<kai-thread>` facades pass their own element,
335
- * so a `card` part's events leave as the bubbling `kai-card` CustomEvent
336
- * instead of being silently discarded. */
337
- cardHostElement?: HTMLElement;
338
- /** Whether this is a user message (right-aligned bubble) vs an assistant
339
- * message (full-width transparent). */
340
- isUser: boolean;
341
- /** Whether text parts render as markdown. */
342
- markdown: boolean;
343
- /** Action-bar entries — built-in names and/or custom descriptors. When empty
344
- * the bar is not rendered. */
345
- actions?: (ChatMessageAction | CustomAction)[];
346
- /** `'always'` (default) keeps the bar visible; `'hover'` reveals it on parent
347
- * `.group` hover. */
348
- actionsReveal?: 'always' | 'hover';
349
- /** Fired with the built-in name or custom id when an action is clicked. */
350
- onAction?: (id: string) => void;
351
- /** The currently-active feedback vote, so the bar can mark like/dislike and
352
- * hide the other. */
353
- activeFeedback?: FeedbackVote;
354
- /** When true, the copy button shows its "copied" check icon. */
355
- copied?: boolean;
356
- /** Whether this message is the one currently streaming. Forwarded to each
357
- * reasoning part's `<Reasoning>` so the disclosure auto-opens while the
358
- * model is thinking and settles back once the stream ends. Without it the
359
- * user watches a static collapsed "Reasoning" label for the whole thinking
360
- * window. The caller owns the definition of "streaming" — for
361
- * `ChatThread` that is `loading` + being the last assistant message. */
362
- isStreaming?: boolean;
363
- /** How a `reasoning` part renders. `'full'` (default) is the current
364
- * behavior: the collapsible `<Reasoning>` disclosure, with the "Thinking…"
365
- * shimmer on its trigger while `isStreaming`. `'compact'` drops the
366
- * disclosure entirely and shows only a shimmer loader while the part is
367
- * streaming — nothing once it settles, so there is no expandable detail to
368
- * open. `'off'` renders the part not at all, in either state. Kit-decides-
369
- * HOW: this is a display-mode fact about the medium, not a per-message
370
- * toggle, so it applies uniformly to every reasoning part in the body. */
371
- reasoningMode?: 'full' | 'compact' | 'off';
372
- /** Seeds the reasoning disclosure open AND keeps it tracking the stream
373
- * (open while streaming, closes when it settles): the pre-Task-19f `full`
374
- * behavior. Default false/absent: the panel starts closed (just the
375
- * "Thinking" shimmer chip) and only opens on click, the current default
376
- * (owner ruling, 2026-08-26). Meaningless when `reasoningMode` is
377
- * `'compact'`/`'off'` (no disclosure exists to open). */
378
- reasoningDefaultOpen?: boolean;
379
- /** INJECT slot projected at the TOP of the body — above the reasoning / tools /
380
- * content. A per-message header: a model-name label, a role + timestamp line.
381
- * In the `<kai-message>` shadow this is `<slot name="before-body" />`. */
382
- beforeBody?: JSX.Element;
383
- /** INJECT slot projected at the BOTTOM of the body — below the action bar. A
384
- * citation / sources row, a token-cost / latency line. In the `<kai-message>`
385
- * shadow this is `<slot name="after-body" />`. */
386
- afterBody?: JSX.Element;
387
- }
388
-
389
- /** One render group over an ordered `parts` array. Two part types collapse runs:
390
- * consecutive `file` parts become a single `'files'` group so they share one
391
- * `<Attachments>` row (matching the pre-parts layout) instead of each opening
392
- * its own, and consecutive `source` parts become a single `'sources'` group so
393
- * the N citations one search produced are ONE wrapped row rather than N stacked
394
- * rows. Every other part is its own `'single'` group. Pure and order-preserving:
395
- * it only decides where the wrapper boundaries fall, never reorders or drops
396
- * anything, so a group sits exactly where its parts sat in `parts`. */
397
- export type MessagePartGroup =
398
- | { kind: 'single'; part: Exclude<MessagePart, { type: 'file' } | { type: 'source' }> }
399
- | { kind: 'files'; parts: Extract<MessagePart, { type: 'file' }>[] }
400
- | { kind: 'sources'; parts: Extract<MessagePart, { type: 'source' }>[] };
401
-
402
- export function groupMessageParts(parts: MessagePart[]): MessagePartGroup[] {
403
- const groups: MessagePartGroup[] = [];
404
- for (const part of parts) {
405
- if (part.type === 'file') {
406
- const last = groups[groups.length - 1];
407
- if (last?.kind === 'files') {
408
- groups[groups.length - 1] = { kind: 'files', parts: [...last.parts, part] };
409
- continue;
410
- }
411
- groups.push({ kind: 'files', parts: [part] });
412
- continue;
413
- }
414
- if (part.type === 'source') {
415
- const last = groups[groups.length - 1];
416
- if (last?.kind === 'sources') {
417
- groups[groups.length - 1] = { kind: 'sources', parts: [...last.parts, part] };
418
- continue;
419
- }
420
- groups.push({ kind: 'sources', parts: [part] });
421
- continue;
422
- }
423
- groups.push({ kind: 'single', part });
424
- }
425
- return groups;
426
- }
427
-
428
- /** The citation chip's label. `index` when the model numbered its citations;
429
- * otherwise `undefined` so `SourceTrigger` falls back to the domain. A source
430
- * with NO url has no domain to fall back to, so its title (then a generic word)
431
- * stands in rather than rendering an empty chip. */
432
- function citationLabel(s: MessageSource): string | number | undefined {
433
- if (s.index !== undefined) return s.index;
434
- if (s.url) return undefined;
435
- return s.title || 'Source';
436
- }
437
-
438
- /** The hover card's headline. Every field is optional, so fall back through
439
- * title -> url -> a generic word. */
440
- function citationTitle(s: MessageSource): string {
441
- return s.title || s.url || 'Source';
442
- }
443
-
444
- /** Narrow a group to one variant IN A SINGLE READ.
445
- *
446
- * The render body below reads its group/part through accessors (see the
447
- * `<Index>` note in `MessageBody`), and TypeScript cannot narrow a
448
- * discriminated union across two separate accessor CALLS —
449
- * `g().kind === 'files' && g()` leaves the second call widened, because as far
450
- * as the compiler knows the two calls could return different values. So the
451
- * test and the cast happen together, on one already-read value. Returns
452
- * `false` rather than `undefined` so it reads as a plain falsy `<Match when>`. */
453
- function groupAs<T extends MessagePartGroup['kind']>(
454
- group: MessagePartGroup,
455
- kind: T,
456
- ): Extract<MessagePartGroup, { kind: T }> | false {
457
- return group.kind === kind ? (group as Extract<MessagePartGroup, { kind: T }>) : false;
458
- }
459
-
460
- /** The same single-read narrowing for `MessagePart`. */
461
- function partAs<T extends MessagePart['type']>(
462
- part: MessagePart,
463
- type: T,
464
- ): Extract<MessagePart, { type: T }> | false {
465
- return part.type === type ? (part as Extract<MessagePart, { type: T }>) : false;
466
- }
467
-
468
- /**
469
- * The shared message body: the message's `parts` rendered in a single ordered
470
- * pass (text, reasoning, tool calls, generative-UI cards, citations and file
471
- * attachments interleaved exactly as they appear), followed by the action bar.
472
- * Runs of `source` and `file` parts each collapse into one row. This is the
473
- * single source of truth for how a message renders, consumed by `ChatThread`
474
- * (the `<For>` over `messages`), the standalone `<kai-message>` facade, and (in
475
- * future) `kai-compare` for each candidate. Pure/prop-driven: all interaction
476
- * state (copied, feedback vote) is owned above and passed in.
477
- */
478
- function MessageBody(props: MessageBodyProps) {
479
- const groups = createMemo(() => groupMessageParts(props.parts));
480
- return (
481
- <>
482
- {/* before-body (inject): a per-message header above everything else. */}
483
- <Show when={props.beforeBody}>{props.beforeBody}</Show>
484
- {/* <Index>, NOT <For>, on purpose — this is load-bearing.
485
- *
486
- * A streaming message re-renders once per delta with a brand-new `parts`
487
- * array (a new reference IS the re-render signal), and
488
- * `groupMessageParts` allocates fresh wrapper objects on top of that. A
489
- * <For> is REFERENCE-keyed, so every chunk looks like an entirely new
490
- * list and every row is torn down and rebuilt. Everything the user has
491
- * done inside a row dies with it: expanding a tool panel or a reasoning
492
- * block mid-stream silently did nothing, because the disclosure opened
493
- * and was discarded microseconds later by the next token (a live probe
494
- * caught the collapsibles' `createUniqueId()` walking cl-9 -> cl-11 ->
495
- * cl-15 across three deltas).
496
- *
497
- * <Index> keys by POSITION and hands each row its value as a SIGNAL, so
498
- * a row stays mounted while its content keeps updating. That is exactly
499
- * the shape of a stream: the folds behind `parts`
500
- * (appendTextPart/appendReasoningPart/upsertToolPart) only ever append
501
- * to the end or replace one part IN PLACE with the same variant — never
502
- * reorder, never change a part's type — so a part's position is a stable
503
- * identity, and a growing text/reasoning block or a tool patched from
504
- * `input-streaming` to `output-available` reaches the DOM through the
505
- * accessor instead of through a remount.
506
- *
507
- * The trade-off of position-keying: splicing a part out of the MIDDLE of
508
- * a message shifts the rows after it, so their local state (an open
509
- * disclosure) stays with the position rather than following the part.
510
- * <For> got that right for a STATIC array and got streaming wrong every
511
- * single time — no consumer splices mid-message, every consumer streams.
512
- *
513
- * This only works while the children read through the accessors below:
514
- * capturing `g().part` once re-freezes the row at its first delta. */}
515
- <Index each={groups()}>
516
- {(group) => (
517
- <Switch fallback={null}>
518
- <Match when={groupAs(group(), 'files')}>
519
- {(g) => (
520
- /* `grid`, NOT `inline`. The inline chip gives an image a 20x20
521
- preview — a thumbnail nobody can read — while the 96px tile
522
- and the hover-card full preview both already existed here and
523
- the thread used neither.
524
-
525
- WHY GRID AND NOT LIST, since `list` shows the filename
526
- outright: `list` stacks (`flex-col`), so a message carrying
527
- four attachments becomes four full-width rows and pushes the
528
- conversation off screen. `grid` is `flex-wrap w-fit`, so the
529
- same four are one 96px-tall row of tiles. Density is what
530
- keeps a thread readable, and it is the whole reason the
531
- thread is not the composer's chip strip.
532
-
533
- A grid tile draws no filename of its own, and hiding one
534
- behind the hover card is not good enough: that serves a
535
- pointer and nothing else. So `<AttachmentInfo>` renders a
536
- VISIBLE truncated caption on every non-image tile — no hover,
537
- no focus, no tap required — and the hover card is an upgrade
538
- to the full name and media type rather than the only way to
539
- get either. */
540
- <Attachments variant="grid" class={props.isUser ? 'mb-2 ml-auto' : 'mb-2'}>
541
- {/* Reference-keyed <For> is right HERE: the run's part objects
542
- are carried over untouched by the folds, and an attachment
543
- holds no state worth preserving. */}
544
- <For each={g().parts}>
545
- {(fp) => (
546
- <Attachment data={fp.attachment}>
547
- <AttachmentHoverCard>
548
- <AttachmentHoverCardTrigger class="block size-full">
549
- <AttachmentPreview />
550
- <AttachmentInfo />
551
- </AttachmentHoverCardTrigger>
552
- <AttachmentHoverCardContent>
553
- {/* An image gets the full preview; everything else
554
- gets the name and type the tile could not fit. */}
555
- <Show
556
- when={
557
- getMediaCategory(fp.attachment) === 'image' &&
558
- fp.attachment.type === 'file' &&
559
- fp.attachment.url
560
- }
561
- fallback={
562
- <>
563
- <div class="text-body font-medium">
564
- {getAttachmentLabel(fp.attachment)}
565
- </div>
566
- <Show when={fp.attachment.mediaType}>
567
- <div class="text-muted-foreground text-caption">
568
- {fp.attachment.mediaType}
569
- </div>
570
- </Show>
571
- </>
572
- }
573
- >
574
- <img
575
- alt={getAttachmentLabel(fp.attachment)}
576
- class="block max-h-64 max-w-xs rounded object-contain"
577
- src={fp.attachment.url}
578
- />
579
- </Show>
580
- </AttachmentHoverCardContent>
581
- </AttachmentHoverCard>
582
- </Attachment>
583
- )}
584
- </For>
585
- </Attachments>
586
- )}
587
- </Match>
588
- <Match when={groupAs(group(), 'sources')}>
589
- {(g) => (
590
- // OUTSIDE the bubble, deliberately. A citation nested inside
591
- // `MessageContent` is indistinguishable from a link the MODEL
592
- // typed into its prose — which is exactly how the first version
593
- // of the S12 conformance check passed while proving nothing.
594
- // `SourceList` is its own container, so the row is a sibling of
595
- // the content part, and `part="citations"` lets a consumer target
596
- // it through the shadow boundary.
597
- <SourceList part="citations" class={props.isUser ? 'justify-end' : undefined}>
598
- {/* Reference-keyed <For> is right HERE, as with files: the
599
- run's part objects are carried over untouched by the folds. */}
600
- <For each={g().parts}>
601
- {(sp) => (
602
- <Source href={sp.source.url}>
603
- <SourceTrigger label={citationLabel(sp.source)} />
604
- <SourceContent
605
- title={citationTitle(sp.source)}
606
- description={sp.source.snippet ?? ''}
607
- />
608
- </Source>
609
- )}
610
- </For>
611
- </SourceList>
612
- )}
613
- </Match>
614
- <Match when={groupAs(group(), 'single')}>
615
- {(g) => {
616
- // An ACCESSOR, never a captured value: the row outlives the
617
- // delta that rebuilt this part, so every read below has to go
618
- // through here for the new content to land. `partAs` does the
619
- // type test and the narrowing cast in one read (see its note).
620
- const part = () => g().part;
621
- const reasoning = () => partAs(part(), 'reasoning');
622
- // A reasoning part with NO text is a round-trip carrier, not
623
- // something to show. Anthropic's redacted_thinking blocks carry
624
- // an opaque blob with no readable text, and the block assembled
625
- // at content_block_stop carries the verbatim payload the encoder
626
- // must echo back. Both are empty-text parts that MUST stay in
627
- // `parts` (the encoder needs them, in order) and must not render
628
- // a blank disclosure.
629
- const carrierOnly = () => { const r = reasoning(); return r !== false && r.text === ''; };
630
- const shownReasoning = () => { const r = reasoning(); return r !== false && r.text !== '' && r; };
631
- return (
632
- <Switch fallback={null}>
633
- <Match when={partAs(part(), 'text')}>
634
- {(p) => (
635
- <MessageContent
636
- part="bubble content"
637
- markdown={props.markdown}
638
- class={props.isUser
639
- // Content token, not the brand token: `--color-primary`
640
- // is the documented consumer brand override
641
- // (theme.css `--kai-color-primary`), so toking message
642
- // TEXT to it means every consumer that brands primary
643
- // gets brand-colored message text. `text-foreground` is
644
- // already `MessageContent`'s base color (matches the
645
- // assistant path's `bg-transparent p-0`, which carries
646
- // no color override and falls through to the same
647
- // base) — dropping `text-primary` here just lets that
648
- // base apply on the user bubble too.
649
- ? 'bg-muted max-w-[85%] rounded-2xl px-4 py-2'
650
- : 'bg-transparent p-0'}
651
- >
652
- {p().text}
653
- </MessageContent>
654
- )}
655
- </Match>
656
- <Match when={carrierOnly()}>{null}</Match>
657
- <Match when={shownReasoning()}>
658
- {(p) => {
659
- // Default 'full' is the pre-existing DISPLAY MODE byte
660
- // for byte; the OPEN behavior changed under Task 19f —
661
- // it no longer auto-opens while streaming by default
662
- // (owner ruling 2026-08-26). `reasoningDefaultOpen`
663
- // reproduces the old always-auto-opens behavior when set.
664
- const mode = () => props.reasoningMode ?? 'full';
665
- return (
666
- <Switch fallback={null}>
667
- <Match when={mode() === 'full'}>
668
- <Reasoning
669
- class="mb-2 w-full"
670
- isStreaming={props.isStreaming}
671
- defaultOpen={props.reasoningDefaultOpen}
672
- openOnStream={props.reasoningDefaultOpen}
673
- >
674
- <ReasoningTrigger>{p().label ?? 'Reasoning'}</ReasoningTrigger>
675
- <ReasoningContent markdown>{p().text}</ReasoningContent>
676
- </Reasoning>
677
- </Match>
678
- {/* 'compact': the same "Thinking…" shimmer the full
679
- disclosure's trigger shows while streaming — no
680
- <Reasoning>/<ReasoningContent>, so there is no
681
- expandable detail and nothing left once the part
682
- settles (isStreaming false → this Match doesn't
683
- fire, matching 'off'). 'off' never reaches here:
684
- its Match doesn't fire either. */}
685
- <Match when={mode() === 'compact' && props.isStreaming}>
686
- <Loader variant="text-shimmer" text={p().label ?? 'Reasoning'} class="mb-2" />
687
- </Match>
688
- </Switch>
689
- );
690
- }}
691
- </Match>
692
- <Match when={partAs(part(), 'tool')}>
693
- {(p) => <Tool toolPart={p().tool} class="mb-2 w-full" />}
694
- </Match>
695
- <Match when={partAs(part(), 'card')}>
696
- {(p) => <CardRenderer envelope={p().envelope} types={props.cardTypes} schemas={props.cardSchemas} hostElement={props.cardHostElement} />}
697
- </Match>
698
- {/* No `source` match here on purpose: source parts never
699
- reach a 'single' group — they are collapsed into a
700
- 'sources' run above and rendered as one citation row. */}
701
- </Switch>
702
- );
703
- }}
704
- </Match>
705
- </Switch>
706
- )}
707
- </Index>
708
- <Show when={(props.actions?.length ?? 0) > 0}>
709
- <MessageActionBar
710
- actions={props.actions!}
711
- reveal={props.actionsReveal === 'hover' ? 'hover' : 'always'}
712
- activeFeedback={props.activeFeedback}
713
- copied={props.copied}
714
- onAction={(id) => props.onAction?.(id)}
715
- />
716
- </Show>
717
- {/* after-body (inject): a citation row / cost line below the action bar. */}
718
- <Show when={props.afterBody}>{props.afterBody}</Show>
719
- </>
720
- );
721
- }
722
-
723
- // --- MessageAction ---
724
-
725
- export interface MessageActionProps {
726
- tooltip: string;
727
- children: JSX.Element;
728
- side?: "top" | "bottom" | "left" | "right";
729
- class?: string;
730
- }
731
-
732
- function MessageAction(props: MessageActionProps) {
733
- return <>{props.children}</>;
734
- }
735
-
736
- // --- MessageCopyButton ---
737
-
738
- export interface MessageCopyButtonProps {
739
- content: string;
740
- size?: number;
741
- class?: string;
742
- }
743
-
744
- function MessageCopyButton(props: MessageCopyButtonProps) {
745
- const [copied, setCopied] = createSignal(false);
746
- const iconSize = () => props.size ?? 14;
747
-
748
- return (
749
- <button
750
- class={props.class}
751
- aria-label={copied() ? 'Copied' : 'Copy message'}
752
- onClick={() => {
753
- navigator.clipboard.writeText(props.content);
754
- setCopied(true);
755
- setTimeout(() => setCopied(false), 2000);
756
- }}
757
- >
758
- <Show when={copied()} fallback={<Copy size={iconSize()} />}>
759
- <Check size={iconSize()} class="text-emerald-400" />
760
- </Show>
761
- </button>
762
- );
763
- }
764
-
765
- export { Message, MessageAvatar, MessageContent, MessageActions, MessageActionBar, MessageBody, MessageAction, MessageCopyButton };