Skip to main content

Message model

How messages are shaped: roles, the common ChatMessage fields, and the structured parts[] body.

Message roles (ChatMessageRole)

Each ChatMessage has role: 'self' | 'peer' | 'assistant' | 'system'.

RoleMeaning
selfMessage from the current user (viewer); aligned to the end side (typically right).
peerMessage from another human (e.g. DM or group); aligned to the start side (typically left), distinct from the assistant bubble until you theme it.
assistantAI / bot; streaming typewriter, optional reasoning, duration, and message-actions apply only here.
systemSystem or informational line; same default alignment as assistant.

Breaking migration from earlier releases: use role: 'self' instead of 'user'. Rename config.userAvatarconfig.selfAvatar, slot user-avatarself-avatar, and add optional peerAvatar / slot peer-avatar for role: 'peer'. For CSS, prefer --chat-self-*; legacy --chat-user-* is still honored via fallbacks inside the components.

ChatMessage fields (common)

FieldDescription
id, roleRequired row identity
partsRequired. Ordered, typed body parts — the single source of truth for the body (see Message body). May be an empty array (e.g. an error-only or streaming-placeholder row).
timestampRow time (used by date separators / footer)
avatarPer-row avatar override (see Per-message avatar)
streaming, error, cancelled, durationAssistant streaming / errors / timing
parentIdWhen set on a message in messages[], that row renders as a compact quote (avatar + body only; no footer or message-actions). Use for reply rows you store in the thread. Distinct from replyMessage, which renders quote blocks under a parent without adding them to messages[] (see Reply blocks).

Message body — parts[]

A message body is always an ordered array of typed parts. This mirrors modern AI chat protocols (Anthropic content blocks, Vercel AI SDK message parts): text, reasoning, tool calls, files, sources, and host‑defined x-* parts all sit side by side and stream/update independently. There is no plain content / reasoning string on ChatMessage.

Part types (MessagePart)

Every part has a stable id (used for keyed rendering + targeted updates) and an optional status ('pending' | 'streaming' | 'complete' | 'error' | 'cancelled').

typeShape (besides id / status)Rendered as
texttext: stringMarkdown bubble (typewriter while status: 'streaming'; charts/Mermaid/forms fences still render)
reasoningtext: stringCollapsible “thinking” block (<i-chat-reasoning>)
tool-calltoolCallId, toolName, title?, args?, state, result?, resultParts?, error?, approval?, durationMs?Tool-call card (<i-chat-tool-call>) — see Tool calls
filemediaType, url? | data? (base64), name?, size?Inline image or download link — see File, source, and custom parts
sourceurl, title?, snippet?Citation link with optional snippet — see File, source, and custom parts
x-* (custom)data: unknownReadable JSON dump — see File, source, and custom parts

Factories

Import helpers so you don’t have to hand-write ids:

import { textPart, reasoningPart, nextPartId, getMessageText } from '@bndynet/ichat';

chat.addMessage({
id: 'a1',
role: 'assistant',
parts: [
reasoningPart('Let me work through this…'),
textPart('The answer is **42**.'),
],
timestamp: Date.now(),
});

// Plain-text view (copy / search / persistence) — joins all text parts:
const plain = getMessageText(chat.messages.find((m) => m.id === 'a1'));
  • textPart(text, opts?) / reasoningPart(text, opts?)opts accepts { id?, status?, metadata? }; an id is generated when omitted.
  • nextPartId(prefix?) — collision-resistant id generator (part-<n>).
  • getMessageText(message) — concatenates all text parts.

Streaming & updating parts

Append and patch parts by id instead of rewriting the whole message:

Method (on <i-chat> / <i-chat-messages>)Description
appendPart(messageId, part)Push a new part (e.g. start a streaming text part, add a tool-call).
updatePart(messageId, partId, patch)Shallow-merge patch into the matching part (e.g. grow text, flip status). Keyed by id, so stateful elements survive.
updateToolCall(messageId, partId, patch)Convenience wrapper around updatePart for tool-call parts.
const id = 'a2';
chat.addMessage({ id, role: 'assistant', parts: [], streaming: true, timestamp: Date.now() });

// Stream a text part:
chat.appendPart(id, textPart('', { id: 'body', status: 'streaming' }));
let acc = '';
for await (const chunk of stream) {
acc += chunk;
chat.updatePart(id, 'body', { text: acc });
}
chat.updatePart(id, 'body', { status: 'complete' });
chat.updateMessage(id, { streaming: false });