ChatMessageListComponent
ChatMessageListComponent is the core primitive for rendering chat messages. It receives an agent input and reads agent.messages(), rendering each message using a matching MessageTemplateDirective. This gives you full control over how each message type is displayed.
Selector: chat-message-list
Import:
import {
ChatMessageListComponent,
MessageTemplateDirective,
getMessageType,
} from '@threadplane/chat';How It Works
- The component receives an
agentinput and readsagent.messages()to get the current message list - For each message, it calls
getMessageType()to determine the template type - It finds the matching
MessageTemplateDirectiveamong its content children - It renders the message using
ngTemplateOutletwith the message as the implicit context
Basic Usage
<chat-message-list [agent]="chatAgent">
<ng-template chatMessageTemplate="human" let-message>
<div class="user-bubble">{{ message.content }}</div>
</ng-template>
<ng-template chatMessageTemplate="ai" let-message>
<div class="ai-message">{{ message.content }}</div>
</ng-template>
<ng-template chatMessageTemplate="tool" let-message>
<pre>{{ message.content }}</pre>
</ng-template>
<ng-template chatMessageTemplate="system" let-message>
<em>{{ message.content }}</em>
</ng-template>
</chat-message-list>API
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
agent | Agent | Required | The agent providing streaming state |
Content Children
The component queries all MessageTemplateDirective instances declared as content children. Each directive declares which message type it handles.
Template Context
Each template receives:
| Variable | Type | Description |
|---|---|---|
$implicit (via let-message) | Message | The runtime-neutral chat message |
index | number | The index of the message in the array |
<ng-template chatMessageTemplate="ai" let-message let-idx="index">
<div>Message #{{ idx }}: {{ message.content }}</div>
</ng-template>MessageTemplateDirective
The MessageTemplateDirective is a structural directive applied to ng-template elements. It declares which message type the template should handle.
Selector: ng-template[chatMessageTemplate]
import { MessageTemplateDirective } from '@threadplane/chat';Input
| Input | Type | Description |
|---|---|---|
chatMessageTemplate | MessageTemplateType | The message type this template handles |
MessageTemplateType
type MessageTemplateType = 'human' | 'ai' | 'tool' | 'system' | 'function';getMessageType()
The getMessageType() function maps a runtime-neutral Message role to a MessageTemplateType.
import { getMessageType } from '@threadplane/chat';
const type = getMessageType(message); // 'human' | 'ai' | 'tool' | 'system' | 'function'Mapping logic:
| Runtime-neutral role | Returns |
|---|---|
'user' | 'human' |
'assistant' | 'ai' |
'tool' | 'tool' |
'system' | 'system' |
| Any other value | 'ai' (default fallback) |
Working with Message Content
Runtime-neutral messages have a content property that can be either a string or a structured array. The library exports a messageContent() utility (used internally by compositions) that handles both cases:
// If content is a string, returns it directly
// If content is structured, serializes to JSON
function messageContent(message: Message): stringFor custom templates, you can access message.content directly and handle the type yourself:
<ng-template chatMessageTemplate="ai" let-message>
@if (typeof message.content === 'string') {
<div [innerHTML]="renderMd(message.content)"></div>
} @else {
<pre>{{ message.content | json }}</pre>
}
</ng-template>typeof message.content === 'string' works directly in the template — content is string | ContentBlock[], so the string branch renders markdown and the array branch falls back to serialized JSON. (If you prefer a named guard, the exported messageContent() utility above collapses both cases to a string for you.)
Full Example
import { Component, inject, ChangeDetectionStrategy, signal } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { injectAgent } from '@threadplane/langgraph';
import {
ChatMessageListComponent,
MessageTemplateDirective,
renderMarkdown,
} from '@threadplane/chat';
@Component({
selector: 'app-messages-demo',
standalone: true,
imports: [ChatMessageListComponent, MessageTemplateDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<chat-message-list [agent]="chatAgent">
<ng-template chatMessageTemplate="human" let-message let-idx="index">
<div style="display: flex; justify-content: flex-end; margin-bottom: 1rem;">
<div style="background: var(--tplane-chat-primary); color: var(--tplane-chat-on-primary); border-radius: var(--tplane-chat-radius-bubble); padding: 0.5rem 1rem; max-width: 70%;" [attr.data-message-index]="idx">
{{ message.content }}
</div>
</div>
</ng-template>
<ng-template chatMessageTemplate="ai" let-message>
<div style="display: flex; gap: 0.75rem; margin-bottom: 1rem;">
<div style="flex: 1;" [innerHTML]="renderMd(message.content)"></div>
</div>
</ng-template>
<ng-template chatMessageTemplate="tool" let-message>
<div style="background: var(--tplane-chat-surface-alt); border-radius: var(--tplane-chat-radius-card); padding: 0.75rem; font-family: var(--tplane-chat-font-mono); font-size: var(--tplane-chat-font-size-sm); margin-bottom: 1rem;">
{{ message.content }}
</div>
</ng-template>
<ng-template chatMessageTemplate="system" let-message>
<div style="text-align: center; color: var(--tplane-chat-text-muted); font-size: var(--tplane-chat-font-size-xs); font-style: italic; margin-bottom: 1rem;">
{{ message.content }}
</div>
</ng-template>
</chat-message-list>
`,
})
export class MessagesDemoComponent {
private sanitizer = inject(DomSanitizer);
// Configured at the app root via provideAgent({ assistantId, threadId, ... }).
chatAgent = injectAgent();
renderMd(content: string | unknown) {
if (typeof content !== 'string') return '';
return renderMarkdown(content, this.sanitizer);
}
}