Overview
Opinionated conversation surface bundling message list, composer, attachments, and scroll affordances for AI or support transcripts.
Usage
[!IMPORTANT] Chat needs a definite-height ancestor Chat's root element fills its parent (
height: 100%). Place it inside an ancestor with a resolved height—a fixed height, a flex or grid cell, orheight: 100dvh—or the message viewport collapses and Chat renders as a small card instead of filling its space. See Layout and sizing.
Adapter-driven streaming
ChatAdapter.sendMessage deliberately stays a Promise<void> command. The
adapter owns the conversation snapshot, while Chat owns the transient visual
stream buffer. Pair the public streaming builders with the imperative instance
methods so both surfaces advance together:
The assistant placeholder must exist before beginStreaming; update the same
snapshot for each token, finalize it before endStreaming, and cancel it when
the backend fails. The package's Adapter-driven streaming example uses a
complete finite stream without assuming a particular model provider.
[!WARNING]
ChatAdapter.subscriberuns inside Chat's own effect Chat openssubscribefrom inside its internal mount$effect, so a synchronous$statewrite insidesubscribecan throweffect_update_depth_exceeded. Defer it withqueueMicrotaskortick(). The same applies ifsubscribereplays a buffered event by calling a handler (onMessage,onTypingChange, etc.) synchronously before returning — defer the CALL to the handler itself, not just any write of your own, sinceonTypingChange/onReadReceiptwrite Chat's own internal state before your code runs at all. See theChatAdapter.subscribeandChatPushHandlersJSDoc for the full explanation and working examples.
Conversation data contract
Chat declares @lostgradient/cinder and svelte as peer dependencies — host
applications install those alongside @lostgradient/chat. conversationalist
(and its own zod dependency) is an implementation detail Chat owns: it
ships as a regular dependency of @lostgradient/chat and installs
automatically, so using Chat never requires a host application to add or
version-pick it. Import the conversation types, builders,
CURRENT_SCHEMA_VERSION, and isJSONValue from @lostgradient/chat rather
than importing conversationalist yourself. An application that uses
conversationalist APIs beyond what Chat re-exports may still depend on it
directly — that is supported, it just is not something Chat requires.
The exported schema version comes from the conversationalist version Chat
depends on. Histories produced by an older compatible schema can render
as-is; a newer schema causes Chat to emit a console warning and requires
upgrading @lostgradient/chat before relying on that history.
Layout and sizing
Chat's root (.chat-container) is height: 100%. When no ancestor on the
chain resolves to a definite height, that 100% resolves against auto and
Chat collapses to the intrinsic height of its empty state plus composer. There
is no browser error: it just silently shrinks to a small card. Give Chat a
sized ancestor with one of the three patterns below.
Fixed-height container. The simplest option—wrap Chat in an element with an explicit height. This is what every example in this component uses.
Full-viewport flex column. For an app shell where Chat should fill the
remaining space, give the column a definite height and let the Chat cell
flex: 1 with min-height: 0 (so it can shrink below its content and scroll
internally rather than overflowing the page).
Grid cell. The same shape works in a grid: give the container a resolved
track (grid-template-rows: auto 1fr), place Chat in the 1fr row, and keep
min-height: 0 on the Chat cell so it can shrink and scroll internally instead
of overflowing the page.
Building composer overlays
Slash-command, mention, and autocomplete overlays should use Chat's composer
API instead of querying .chat-input-editor directly.
Use bind:this to read or control the composer:
getComposerValue() returns the current plain-text value, clearInput()
clears it, and getEditorElement() returns the textarea element (or null
before mount and after teardown) for overlay anchoring and focus management.
insertAtRange({ start, end }, text) replaces that composer range without a
synthetic DOM event, focuses the textarea, and places the caret after the
inserted text. It uses native HTMLTextAreaElement.setRangeText() boundaries:
out-of-bounds indexes are clamped and a reversed range throws a DOMException.
Calls before mount or after teardown are safe no-ops.
oncomposerkeydown runs before Chat's internal Enter-to-send handling for
normal composer keydowns. If the callback calls event.preventDefault(), Chat
skips its internal key handling for that event. Chat does not call the hook
during IME composition, so Enter can still confirm the active candidate instead
of sending or being consumed by an overlay.
For ARIA combobox patterns, pass textarea-specific attributes through the composer-prefixed props:
Announcing custom action-required rows
Chat owns always-rendered polite and assertive live regions outside the
role="log" timeline. If you render action-required UI through a custom row
or messagePart snippet, announce it through Chat instead of mounting another
aria-live region inside the log:
Use the default polite channel for non-urgent status updates:
Consumer announcements clear after a short interval so Chat's own history,
unread, typing, and tool-approval announcements can continue to flow.
Built-in tool-approval rows keep precedence on the assertive channel. If a
consumer assertive announcement races with Chat's derived
Action required: ... tool-approval announcement, Chat announces the built-in
tool approval and drops the consumer assertive text to avoid double output.
Per-row snippet context
row, messageActions, and messageStatus receive the same
ChatRowContext. The context contains the message that owns the visible row and
optional resolved toolCallPair and artifact values:
messageActions is rendered in the same action row as Chat's built-in copy
and edit controls. For native buttons, add the shared
chat-message-action-button class to opt into the action-row sizing, focus,
hover, and touch styles:
The class is intentionally opt-in so consumers can use their own button
component or visual treatment without Chat overriding it. Cinder Button
instances already provide their own complete styling and do not need this
class.
A paired tool-result message does not render a second visible row and does not
invoke the snippets separately. Chat folds that result into the corresponding
tool-call row's toolCallPair and resolves cinder:artifact metadata from the
folded result into artifact. Metadata on the visible message takes precedence.
Ordinary rows and unpaired tool-result rows resolve their own artifact metadata,
so consumers do not need an external message lookup or a hand-rolled type guard.
Guidance
Use When
- Shipping a full chat surface with composer, scroll-anchor, unread indicator, and attachments bundled as one heavyweight drop-in.
- Building an AI assistant or support thread where conversation state is modeled as a transcript of role-tagged messages.
Avoid When
- Rendering a one-off message list — compose lighter primitives directly instead of pulling the full suite.
- The transcript is read-only and needs no composer — a simple list of message bubbles is a better fit.
Attachment serialization
When capabilities.attachments is enabled, onsubmit receives ready
ChatAttachment[] values. Use serializeChatAttachment() or
serializeChatAttachments() from @lostgradient/chat to convert those
files into transportable base64 payloads without spreading the full byte array
onto the JavaScript stack.
Each serialized attachment has this shape:
The output is intentionally the base64 source payload for conversationalist's
proposed DocumentContent bridge in stevekinney/agent-bureau#153. A consumer
can wrap it as { type: 'document', name, mimeType, source: { kind: 'base64', data: content } } when adding composer attachments to conversation state.
When to use
- Shipping a full chat surface with composer, scroll-anchor, unread indicator, and attachments bundled as one heavyweight drop-in.
- Building an AI assistant or support thread where conversation state is modeled as a transcript of role-tagged messages.
- Embedding Chat in a definite-height flex or grid region where its internal transcript should own scrolling.
- Rendering a one-off message list — compose lighter primitives directly instead of pulling the full suite.
- The transcript is read-only and needs no composer — a simple list of message bubbles is a better fit.
Examples
Adapter-driven streaming
Use ChatAdapter.sendMessage with the public streaming builders while keeping the conversation snapshot synchronized with Chat's imperative stream.
Basic chat
A conversation with editable input and markdown rendering.
Density and variant
Compare comfortable vs compact density and bubble vs flat visual variant on the same conversationalist-shaped transcript.
Full-height layout
Place Chat in a definite-height flex or grid cell with min-height: 0 so the transcript owns the scroll region.
Interactive harness
Drive Chat from a control panel: reply as the other side (instant / typing / streaming), inject tool calls, toggle features, and watch every callback fire.
Streaming chat
Streaming status keeps the composer in stop mode.
With reasoning and steps
Assistant messages can surface a collapsible reasoning block (extended thinking) and an ordered step list before the final answer. Both are UI-only overlays derived from message metadata — the underlying transcript is unchanged.
With suggested replies
After the final assistant message, suggested follow-up labels appear as clickable chips. Clicking a chip calls onSuggestionSelect(label). The chips are derived from message metadata — the underlying transcript is unchanged.
With tool approval
Action-required tool results render as approval prompts the user must accept or reject before the tool can continue.
With tool calls
Tool results fold into the visible row with validated artifact metadata for opening a panel.
Props
| Name | Type | Default | Required | Bindable | Description |
|---|---|---|---|---|---|
conversation | ConversationHistory | req | The conversation transcript to render. Pass a {@link ConversationHistory}
snapshot; consumers holding a stateful conversation object pass its current
snapshot (e.g. conversation.current). | ||
atBottom | boolean | true | bind | Whether the message viewport is scrolled to the bottom. Bindable; updated automatically as the user scrolls. Default true. | |
unreadCount | number | 0 | bind | Number of messages received while the viewport was scrolled away from the bottom. Bindable; resets to 0 when the user scrolls to the bottom. Default 0. | |
newMessageIndicatorVisible | boolean | false | bind | Whether the "new messages" indicator is currently visible above the composer. Bindable; cleared automatically when the viewport reaches the bottom. Default false. |