@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.
- package/README.md +1 -1
- package/dist/agent-tooling/construct/public.d.ts +15 -0
- package/dist/agent-tooling/construct/schema.d.ts +111 -0
- package/dist/construct-cli.es.js +3 -3
- package/dist/construct.d.ts +5 -0
- package/dist/construct.js +327 -0
- package/dist/core-C8fzo39E.js +1 -12
- package/dist/engine-javascript-C1x7zo1_.js +1 -141
- package/dist/mcp.es.js +1 -1
- package/package.json +16 -16
- package/bin/route.test.js +0 -58
- package/src/agent-tooling/README.md +0 -192
- package/src/agent-tooling/archetypes.ts +0 -205
- package/src/agent-tooling/catalog/README.md +0 -676
- package/src/agent-tooling/catalog/catalog-types.ts +0 -231
- package/src/agent-tooling/catalog/fabrications.ts +0 -96
- package/src/agent-tooling/catalog/invariants.ts +0 -284
- package/src/agent-tooling/catalog/labs-titles.ts +0 -114
- package/src/agent-tooling/catalog/scenarios.ts +0 -87
- package/src/agent-tooling/catalog/surfaces.ts +0 -347
- package/src/agent-tooling/construct/cli-entry.ts +0 -8
- package/src/agent-tooling/construct/cli.ts +0 -147
- package/src/agent-tooling/construct/codegen.ts +0 -1635
- package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
- package/src/agent-tooling/construct/dev.ts +0 -148
- package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
- package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
- package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
- package/src/agent-tooling/construct/schema.ts +0 -452
- package/src/agent-tooling/integrations/anthropic.ts +0 -442
- package/src/agent-tooling/integrations/cloudflare.ts +0 -218
- package/src/agent-tooling/integrations/langgraph.ts +0 -133
- package/src/agent-tooling/integrations/mastra.ts +0 -182
- package/src/agent-tooling/integrations/mock.ts +0 -148
- package/src/agent-tooling/integrations/ollama.ts +0 -97
- package/src/agent-tooling/integrations/openai.ts +0 -95
- package/src/agent-tooling/integrations/openrouter.ts +0 -82
- package/src/agent-tooling/integrations/pi.ts +0 -176
- package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
- package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
- package/src/agent-tooling/mcp/manifest.ts +0 -422
- package/src/agent-tooling/mcp/server.ts +0 -116
- package/src/agent-tooling/mcp/stdio.ts +0 -18
- package/src/agent-tooling/mcp/tools/construct.ts +0 -130
- package/src/agent-tooling/mcp/tools/debug.ts +0 -621
- package/src/agent-tooling/mcp/tools/reference.ts +0 -980
- package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
- package/src/agent-tooling/mcp/tools/theme.ts +0 -456
- package/src/agent-tooling/mcp/tools/types.ts +0 -14
- package/src/agent-tooling/mcp/validate-args.ts +0 -141
- package/src/agent-tooling/recipes/composed-thread.ts +0 -714
- package/src/agent-tooling/recipes/index.ts +0 -22
- package/src/agent-tooling/recipes/types.ts +0 -32
- package/src/agent-tooling/registry.ts +0 -132
- package/src/agent-tooling/route-emit.ts +0 -305
- package/src/agent-tooling/types.ts +0 -390
- package/src/components/artifact-card.tsx +0 -120
- package/src/components/artifact.tsx +0 -914
- package/src/components/attachment-types.ts +0 -66
- package/src/components/attachments.tsx +0 -536
- package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
- package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
- package/src/components/audio-visualizer/fit-scale.ts +0 -112
- package/src/components/audio-visualizer/index.tsx +0 -528
- package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
- package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
- package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
- package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
- package/src/components/audio-visualizer/sizes.ts +0 -54
- package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
- package/src/components/audio-visualizer/variant-bar.tsx +0 -175
- package/src/components/audio-visualizer/variant-custom.tsx +0 -344
- package/src/components/audio-visualizer/variant-grid.tsx +0 -175
- package/src/components/audio-visualizer/variant-radial.tsx +0 -163
- package/src/components/audio-visualizer/variant-wave.tsx +0 -130
- package/src/components/audio-visualizer/wave.glsl.ts +0 -130
- package/src/components/card-fallback.tsx +0 -60
- package/src/components/card-renderer.tsx +0 -220
- package/src/components/card.tsx +0 -212
- package/src/components/chain-of-thought.tsx +0 -348
- package/src/components/chat-container.tsx +0 -89
- package/src/components/chat-scope-picker.tsx +0 -57
- package/src/components/chat-thread.tsx +0 -1014
- package/src/components/checkpoint.tsx +0 -88
- package/src/components/choice-card.tsx +0 -743
- package/src/components/coachmark.tsx +0 -249
- package/src/components/code-block.tsx +0 -181
- package/src/components/composer-dom.ts +0 -159
- package/src/components/composer-highlight.ts +0 -242
- package/src/components/composer-history.ts +0 -82
- package/src/components/composer.tsx +0 -1075
- package/src/components/confirm-card.tsx +0 -402
- package/src/components/context.tsx +0 -377
- package/src/components/conversation-item.tsx +0 -241
- package/src/components/conversation-list.tsx +0 -429
- package/src/components/conversation-panel.tsx +0 -118
- package/src/components/dismissed-stub.tsx +0 -80
- package/src/components/embed.tsx +0 -200
- package/src/components/empty.tsx +0 -177
- package/src/components/feedback-bar.tsx +0 -166
- package/src/components/file-tree.tsx +0 -530
- package/src/components/file-upload.tsx +0 -167
- package/src/components/form-widgets.tsx +0 -525
- package/src/components/form.tsx +0 -1296
- package/src/components/home-panel.tsx +0 -159
- package/src/components/image.tsx +0 -65
- package/src/components/link-preview.tsx +0 -195
- package/src/components/loader.tsx +0 -367
- package/src/components/markdown.tsx +0 -162
- package/src/components/message-skills.tsx +0 -40
- package/src/components/message.tsx +0 -765
- package/src/components/model-switcher.tsx +0 -132
- package/src/components/prompt-input.tsx +0 -188
- package/src/components/prompt-suggestion.tsx +0 -150
- package/src/components/reasoning.tsx +0 -293
- package/src/components/response-compare-types.ts +0 -152
- package/src/components/response-compare.tsx +0 -455
- package/src/components/response-stream.tsx +0 -109
- package/src/components/screen.tsx +0 -181
- package/src/components/scroll-button.tsx +0 -93
- package/src/components/source.tsx +0 -192
- package/src/components/tasks-card.tsx +0 -801
- package/src/components/text-shimmer.tsx +0 -37
- package/src/components/thinking-bar.tsx +0 -50
- package/src/components/thread.tsx +0 -228
- package/src/components/toast.tsx +0 -503
- package/src/components/tool-classify.ts +0 -26
- package/src/components/tool-types.ts +0 -49
- package/src/components/tool.tsx +0 -190
- package/src/components/use-card-resolution.ts +0 -44
- package/src/components/voice-input.tsx +0 -222
- package/src/components/voice-output.tsx +0 -242
- package/src/components/widget-tab-bar.tsx +0 -73
- package/src/components/workspace-shell.tsx +0 -285
- package/src/diagnostics/hook.ts +0 -352
- package/src/diagnostics/index.ts +0 -82
- package/src/diagnostics/report-request.ts +0 -296
- package/src/elements/agent-card.tsx +0 -67
- package/src/elements/artifact.tsx +0 -194
- package/src/elements/attachments.tsx +0 -140
- package/src/elements/audio-visualizer.tsx +0 -227
- package/src/elements/autoloader.ts +0 -89
- package/src/elements/avatar.tsx +0 -33
- package/src/elements/badge.tsx +0 -32
- package/src/elements/button.tsx +0 -149
- package/src/elements/card.tsx +0 -133
- package/src/elements/cards.tsx +0 -266
- package/src/elements/chain-of-thought.tsx +0 -141
- package/src/elements/chat-scope-picker.tsx +0 -65
- package/src/elements/chat-types.ts +0 -102
- package/src/elements/chat-workspace.tsx +0 -188
- package/src/elements/chat.tsx +0 -304
- package/src/elements/checkbox-group.tsx +0 -164
- package/src/elements/checkbox.tsx +0 -134
- package/src/elements/checkpoint.tsx +0 -47
- package/src/elements/choice.tsx +0 -100
- package/src/elements/coachmark.tsx +0 -90
- package/src/elements/code-block.tsx +0 -60
- package/src/elements/command.tsx +0 -197
- package/src/elements/compare.tsx +0 -111
- package/src/elements/compiled.css +0 -2
- package/src/elements/composer.tsx +0 -162
- package/src/elements/confirm-card.tsx +0 -76
- package/src/elements/context-meter.tsx +0 -107
- package/src/elements/conversation-item.tsx +0 -120
- package/src/elements/conversation-list.tsx +0 -203
- package/src/elements/css.ts +0 -5
- package/src/elements/default-input.tsx +0 -392
- package/src/elements/define-entry.ts +0 -12
- package/src/elements/define.tsx +0 -563
- package/src/elements/diagnostic-events.ts +0 -114
- package/src/elements/dialog.tsx +0 -145
- package/src/elements/disclosure.ts +0 -95
- package/src/elements/dock.tsx +0 -215
- package/src/elements/dropdown.tsx +0 -123
- package/src/elements/editable-label.tsx +0 -140
- package/src/elements/element-data-types.ts +0 -152
- package/src/elements/element-diagnostics.ts +0 -392
- package/src/elements/element-manifest.json +0 -354
- package/src/elements/element-nonscalar.json +0 -187
- package/src/elements/element-types.d.ts +0 -4353
- package/src/elements/embed.tsx +0 -35
- package/src/elements/empty.tsx +0 -29
- package/src/elements/feedback-bar.tsx +0 -66
- package/src/elements/file-tree.tsx +0 -62
- package/src/elements/file-upload.tsx +0 -44
- package/src/elements/form.tsx +0 -94
- package/src/elements/hover-card.tsx +0 -80
- package/src/elements/icon.tsx +0 -41
- package/src/elements/image.tsx +0 -32
- package/src/elements/input.tsx +0 -492
- package/src/elements/kbd.tsx +0 -45
- package/src/elements/link-preview.tsx +0 -34
- package/src/elements/loader.tsx +0 -25
- package/src/elements/markdown.tsx +0 -38
- package/src/elements/menu.tsx +0 -241
- package/src/elements/message-skills.tsx +0 -83
- package/src/elements/message.tsx +0 -361
- package/src/elements/model-switcher.tsx +0 -121
- package/src/elements/nav.tsx +0 -87
- package/src/elements/notice.tsx +0 -53
- package/src/elements/pane-grid.tsx +0 -111
- package/src/elements/pane-group.tsx +0 -119
- package/src/elements/pane.tsx +0 -129
- package/src/elements/popover.tsx +0 -81
- package/src/elements/progress-bar.tsx +0 -39
- package/src/elements/prompt-dock.tsx +0 -82
- package/src/elements/prompt-input.tsx +0 -240
- package/src/elements/prompt-suggestions.tsx +0 -113
- package/src/elements/radio-group.tsx +0 -129
- package/src/elements/reasoning.tsx +0 -71
- package/src/elements/register-impl.ts +0 -148
- package/src/elements/register.ts +0 -76
- package/src/elements/remote.tsx +0 -225
- package/src/elements/resizable.globals.d.ts +0 -36
- package/src/elements/resizable.tsx +0 -704
- package/src/elements/response-stream.tsx +0 -40
- package/src/elements/screen.tsx +0 -90
- package/src/elements/scroll-area.tsx +0 -33
- package/src/elements/scroll-button.tsx +0 -179
- package/src/elements/search.tsx +0 -185
- package/src/elements/segmented.tsx +0 -129
- package/src/elements/select.tsx +0 -165
- package/src/elements/separator.tsx +0 -36
- package/src/elements/setting-item.tsx +0 -50
- package/src/elements/settings-group.tsx +0 -37
- package/src/elements/skeleton.tsx +0 -45
- package/src/elements/slider.tsx +0 -171
- package/src/elements/slot-text.ts +0 -73
- package/src/elements/slots.ts +0 -787
- package/src/elements/source.tsx +0 -126
- package/src/elements/status.tsx +0 -42
- package/src/elements/styles.css +0 -328
- package/src/elements/switch.tsx +0 -118
- package/src/elements/tabs.tsx +0 -95
- package/src/elements/tasks.tsx +0 -103
- package/src/elements/text-shimmer.tsx +0 -28
- package/src/elements/thinking-bar.tsx +0 -43
- package/src/elements/thread.tsx +0 -139
- package/src/elements/toast.tsx +0 -106
- package/src/elements/tool.tsx +0 -78
- package/src/elements/tooltip.tsx +0 -85
- package/src/elements/validate-messages.ts +0 -76
- package/src/elements/voice-input.tsx +0 -100
- package/src/elements/voice-output.tsx +0 -82
- package/src/index.ts +0 -430
- package/src/primitives/audio-bands.ts +0 -174
- package/src/primitives/card-component-types.ts +0 -63
- package/src/primitives/card-contract.ts +0 -80
- package/src/primitives/card-data-types.ts +0 -361
- package/src/primitives/card-host.tsx +0 -37
- package/src/primitives/card-recovery.ts +0 -160
- package/src/primitives/card-registry.tsx +0 -78
- package/src/primitives/card-resolution.ts +0 -36
- package/src/primitives/card-routing.ts +0 -85
- package/src/primitives/card-schemas/artifact.schema.json +0 -92
- package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
- package/src/primitives/card-schemas/card-event.schema.json +0 -12
- package/src/primitives/card-schemas/choice.schema.json +0 -75
- package/src/primitives/card-schemas/confirm.schema.json +0 -65
- package/src/primitives/card-schemas/embed.schema.json +0 -63
- package/src/primitives/card-schemas/form.result.schema.json +0 -7
- package/src/primitives/card-schemas/form.schema.json +0 -53
- package/src/primitives/card-schemas/link.schema.json +0 -54
- package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
- package/src/primitives/card-schemas/tasks.schema.json +0 -83
- package/src/primitives/card-tags.ts +0 -61
- package/src/primitives/card-validate-cards.ts +0 -255
- package/src/primitives/card-validate-schemas.ts +0 -58
- package/src/primitives/card-validate.ts +0 -131
- package/src/primitives/chat-config.tsx +0 -103
- package/src/primitives/composer-model.ts +0 -49
- package/src/primitives/composer-triggers.ts +0 -35
- package/src/primitives/controllable.ts +0 -21
- package/src/primitives/conversation-store.ts +0 -248
- package/src/primitives/create-kai-chat.ts +0 -66
- package/src/primitives/create-tween.ts +0 -266
- package/src/primitives/embed-providers.ts +0 -254
- package/src/primitives/field-mask.ts +0 -256
- package/src/primitives/field-semantics.ts +0 -115
- package/src/primitives/highlighter.ts +0 -157
- package/src/primitives/input-mask.ts +0 -853
- package/src/primitives/link-preview.ts +0 -87
- package/src/primitives/message-feedback.ts +0 -124
- package/src/primitives/pdf-preview.ts +0 -121
- package/src/primitives/toast-store.ts +0 -338
- package/src/primitives/url-scheme-policy.ts +0 -70
- package/src/primitives/use-audio-analysis.ts +0 -340
- package/src/primitives/use-auto-resize.ts +0 -31
- package/src/primitives/use-resize-observer.ts +0 -45
- package/src/primitives/use-sequencer.ts +0 -72
- package/src/primitives/use-speech-recognition.ts +0 -146
- package/src/primitives/use-stick-to-bottom.ts +0 -75
- package/src/primitives/use-text-stream.ts +0 -143
- package/src/primitives/use-voice-recorder.ts +0 -62
- package/src/primitives/visualizer-sequences.ts +0 -350
- package/src/remote/host-embed.ts +0 -345
- package/src/remote/index.ts +0 -2
- package/src/remote/origin.ts +0 -30
- package/src/remote/provider-runtime.ts +0 -262
- package/src/remote/provider.ts +0 -2
- package/src/remote/validate.ts +0 -22
- package/src/remote/version.ts +0 -12
- package/src/remote/wire.ts +0 -48
- package/src/schemas/from-tool-call.ts +0 -151
- package/src/schemas/index.ts +0 -341
- package/src/schemas/provider-subsets.ts +0 -538
- package/src/schemas/registry.ts +0 -355
- package/src/schemas/tool-defs.ts +0 -793
- package/src/solid.ts +0 -173
- package/src/state/index.ts +0 -44
- package/src/state/messages.ts +0 -53
- package/src/state/mock.ts +0 -297
- package/src/state/parts.ts +0 -295
- package/src/state/persistence.ts +0 -180
- package/src/state/stream.ts +0 -246
- package/src/state/suggestions.ts +0 -9
- package/src/state/threads.ts +0 -142
- package/src/test-utils/fake-clock.ts +0 -88
- package/src/types.ts +0 -95
- package/src/ui/action-icons.ts +0 -53
- package/src/ui/agent-card.tsx +0 -218
- package/src/ui/avatar.tsx +0 -23
- package/src/ui/badge.tsx +0 -21
- package/src/ui/button.tsx +0 -49
- package/src/ui/card.tsx +0 -245
- package/src/ui/checkbox-group.tsx +0 -153
- package/src/ui/checkbox.tsx +0 -63
- package/src/ui/collapsible.tsx +0 -163
- package/src/ui/command.tsx +0 -103
- package/src/ui/dialog.tsx +0 -223
- package/src/ui/dock.tsx +0 -630
- package/src/ui/dropdown.tsx +0 -636
- package/src/ui/editable-label.tsx +0 -112
- package/src/ui/hover-card.tsx +0 -345
- package/src/ui/icon.tsx +0 -167
- package/src/ui/input.tsx +0 -391
- package/src/ui/kbd.tsx +0 -118
- package/src/ui/nav.tsx +0 -282
- package/src/ui/notice.tsx +0 -84
- package/src/ui/overlay.tsx +0 -277
- package/src/ui/pane-grid.tsx +0 -117
- package/src/ui/pane-group.tsx +0 -291
- package/src/ui/pane.tsx +0 -193
- package/src/ui/popover.tsx +0 -106
- package/src/ui/progress-bar.tsx +0 -83
- package/src/ui/prompt-dock.tsx +0 -145
- package/src/ui/radio.tsx +0 -150
- package/src/ui/resizable.tsx +0 -693
- package/src/ui/scroll-area.tsx +0 -26
- package/src/ui/segmented.tsx +0 -109
- package/src/ui/select.tsx +0 -168
- package/src/ui/separator.tsx +0 -10
- package/src/ui/settings-group.tsx +0 -67
- package/src/ui/skeleton.tsx +0 -73
- package/src/ui/slider.tsx +0 -178
- package/src/ui/status.tsx +0 -48
- package/src/ui/switch.tsx +0 -119
- package/src/ui/tabs.tsx +0 -168
- package/src/ui/textarea.tsx +0 -21
- package/src/ui/tooltip.tsx +0 -118
- package/src/utils/cn.ts +0 -30
- package/src/wire/chunk.ts +0 -248
- package/src/wire/consume.ts +0 -599
- package/src/wire/diagnostics.ts +0 -727
- package/src/wire/encode-probe.ts +0 -214
- package/src/wire/encode.ts +0 -947
- package/src/wire/files.ts +0 -342
- package/src/wire/formats/anthropic.ts +0 -274
- package/src/wire/formats/openai.ts +0 -228
- package/src/wire/index.ts +0 -95
- package/src/wire/media-types.ts +0 -344
- package/src/wire/read.ts +0 -396
- package/src/wire/sink-helpers.ts +0 -50
- 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 };
|