Headless
If the built-in <Chatbot> doesn't fit your design, skip it entirely
and build your own UI on top of the useChannel hook. The chat panel
below is 100% custom — none of the SDK's stylesheets are imported.
Fully Custom UI
Skip the <Chatbot> component entirely. Use the useChannel hook to read the conversation and call sendMessage, then build whatever UI matches your brand.
The interface below is 100% custom — none of the SDK's built-in chat UI is used.
Example
import { useMemo, useState } from "react";
import { AsgardServiceClient } from "@asgard-js/core";
import { useChannel } from "@asgard-js/react";
export function HeadlessChat() {
const client = useMemo(
() =>
new AsgardServiceClient({
botProviderEndpoint: "https://...",
}),
[],
);
const { conversation, sendMessage, isConnecting } = useChannel({
client,
customChannelId: "my-channel",
defaultIsOpen: true,
});
const messages = conversation
? Array.from(conversation.messages.values())
: [];
return (
<div>
{messages.map((msg) => {
if (msg.type === "user") return <div key={msg.messageId}>{msg.text}</div>;
if (msg.type === "bot") return <div key={msg.messageId}>{msg.message?.text}</div>;
if (msg.type === "tool-call")
return <div key={msg.messageId}>🔧 {msg.toolName}</div>;
return null;
})}
{/* Your own input → sendMessage({ text }) */}
</div>
);
}
useChannel Return Value
| Field | Description |
|---|---|
conversation | Current Conversation containing a messages Map |
sendMessage | Send a message (supports text / payload / blobIds) |
resetChannel | Reset the channel and start a new conversation |
closeChannel | Close the SSE connection |
channelTitle | Current channel title (null = unnamed); updates from the backend title.update event |
isConnecting | Whether the channel is connecting |
isResetting | Whether the channel is resetting |
Message Types
All messages share the ConversationMessage union — branch on type
when rendering:
user— sent by the userbot— bot reply.bot.message.templatecarries the built-in SDK template; streaming text is onbot.typingText, the full text is onbot.message.texttool-call— agent tool invocationthinking— extended-thinking (reasoning) block:thinking.textholds the content,thinking.isThinkingdistinguishes streaming vs. completedsubagent— subagent lifecycle:subagent.kind(start/complete),status,summaryerror— connection or runtime error
Derived-State Hooks (tasks / subagents / title)
Besides walking conversation.messages yourself, @asgard-js/react also exposes three hooks that bridge the reactive derived-state stores on Channel into useSyncExternalStore, re-rendering only when that slice changes:
useTaskList(channel)→Task[](see Task Check List)useSubagents(channel)→Subagent[](see Subagent List)useChannelTitle(channel)→string | null(see Channel Title)
The framework-agnostic building blocks live in @asgard-js/core: createDerivedStores(conversation$), deriveTasks(conversation), deriveSubagents(conversation).