Page actions

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

  1. The component receives an agent input and reads agent.messages() to get the current message list
  2. For each message, it calls getMessageType() to determine the template type
  3. It finds the matching MessageTemplateDirective among its content children
  4. It renders the message using ngTemplateOutlet with 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

InputTypeDefaultDescription
agentAgentRequiredThe 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:

VariableTypeDescription
$implicit (via let-message)MessageThe runtime-neutral chat message
indexnumberThe 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

InputTypeDescription
chatMessageTemplateMessageTemplateTypeThe 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 roleReturns
'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 is either a string or a ContentBlock[]. The library exports a messageContent() utility that flattens either shape to a string:

// If content is a string, returns it directly.
// If content is a block array, concatenates the visible text blocks
// (`text` and `output_text`) and skips reasoning, tool-use, and image blocks.
function messageContent(message: { content: unknown }): string

The parameter is structural: the function reads nothing but content. Pass it the Message handed to a chatMessageTemplate straight out of agent.messages(), or a LangChain BaseMessage from a LangChain-shaped source. Both type-check, and neither needs a cast.

For custom templates, access message.content directly and narrow the type in the component class. Angular template expressions have no typeof operator, so the check has to live in a method:

import { Component } from '@angular/core';
import { JsonPipe } from '@angular/common';
import {
  ChatMessageListComponent,
  MessageTemplateDirective,
  type Agent,
} from '@threadplane/chat';
 
@Component({
  selector: 'app-my-list',
  imports: [ChatMessageListComponent, MessageTemplateDirective, JsonPipe],
  template: `
    <chat-message-list [agent]="agent">
      <ng-template chatMessageTemplate="ai" let-message>
        @if (isText(message.content)) {
          <p>{{ message.content }}</p>
        } @else {
          <pre>{{ message.content | json }}</pre>
        }
      </ng-template>
    </chat-message-list>
  `,
})
export class MyListComponent {
  agent!: Agent;
 
  isText(content: unknown): content is string {
    return typeof content === 'string';
  }
}

The string branch renders markdown; the array branch falls back to serialized JSON.

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);
  }
}

Looking for something specific?