diff --git a/docs/data/chat/behavior/error-handling/error-handling.md b/docs/data/chat/behavior/error-handling/error-handling.md
index 60785f4624100..1740311f1f17c 100644
--- a/docs/data/chat/behavior/error-handling/error-handling.md
+++ b/docs/data/chat/behavior/error-handling/error-handling.md
@@ -3,7 +3,7 @@ productId: x-chat
title: Error Handling
packageName: '@mui/x-chat'
githubLabel: 'scope: chat'
-components: ChatBox, MessageError
+components: ChatBox, ChatMessageError, MessageError
---
# Chat - Error Handling
diff --git a/docs/data/chatApiPages.ts b/docs/data/chatApiPages.ts
index 7be6ed1fbe5a2..1e642f3f8b5a3 100644
--- a/docs/data/chatApiPages.ts
+++ b/docs/data/chatApiPages.ts
@@ -93,6 +93,10 @@ const chatApiPages: MuiPage[] = [
pathname: '/x/api/chat/chat-message-content',
title: 'ChatMessageContent',
},
+ {
+ pathname: '/x/api/chat/chat-message-error',
+ title: 'ChatMessageError',
+ },
{
pathname: '/x/api/chat/chat-message-group',
title: 'ChatMessageGroup',
diff --git a/docs/pages/x/api/chat/chat-date-divider.json b/docs/pages/x/api/chat/chat-date-divider.json
index 8a2355526b6e5..821827e8ddf06 100644
--- a/docs/pages/x/api/chat/chat-date-divider.json
+++ b/docs/pages/x/api/chat/chat-date-divider.json
@@ -84,6 +84,12 @@
"description": "Styles applied to the message meta element.",
"isGlobal": false
},
+ {
+ "key": "noAvatar",
+ "className": "MuiChatDateDivider-noAvatar",
+ "description": "Applied when the avatar slot is hidden (`slots.avatar: null`) so the grid drops the avatar track.",
+ "isGlobal": false
+ },
{
"key": "roleAssistant",
"className": "MuiChatDateDivider-roleAssistant",
diff --git a/docs/pages/x/api/chat/chat-layout.json b/docs/pages/x/api/chat/chat-layout.json
index 3bbbecbb69847..b48503f9a68bc 100644
--- a/docs/pages/x/api/chat/chat-layout.json
+++ b/docs/pages/x/api/chat/chat-layout.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ChatLayout",
- "imports": [
- "import { ChatLayout } from '@mui/x-chat/headless';",
- "import { ChatLayout } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ChatLayout } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "root", "description": "", "class": null },
{ "name": "conversationsPane", "description": "", "class": null },
diff --git a/docs/pages/x/api/chat/chat-message-actions.json b/docs/pages/x/api/chat/chat-message-actions.json
index c1d3e17f02c8c..2cd644b7d6905 100644
--- a/docs/pages/x/api/chat/chat-message-actions.json
+++ b/docs/pages/x/api/chat/chat-message-actions.json
@@ -84,6 +84,12 @@
"description": "Styles applied to the message meta element.",
"isGlobal": false
},
+ {
+ "key": "noAvatar",
+ "className": "MuiChatMessageActions-noAvatar",
+ "description": "Applied when the avatar slot is hidden (`slots.avatar: null`) so the grid drops the avatar track.",
+ "isGlobal": false
+ },
{
"key": "roleAssistant",
"className": "MuiChatMessageActions-roleAssistant",
diff --git a/docs/pages/x/api/chat/chat-message-author-label.json b/docs/pages/x/api/chat/chat-message-author-label.json
index 8a7898292481a..2cb45a2b755f2 100644
--- a/docs/pages/x/api/chat/chat-message-author-label.json
+++ b/docs/pages/x/api/chat/chat-message-author-label.json
@@ -84,6 +84,12 @@
"description": "Styles applied to the message meta element.",
"isGlobal": false
},
+ {
+ "key": "noAvatar",
+ "className": "MuiChatMessageAuthorLabel-noAvatar",
+ "description": "Applied when the avatar slot is hidden (`slots.avatar: null`) so the grid drops the avatar track.",
+ "isGlobal": false
+ },
{
"key": "roleAssistant",
"className": "MuiChatMessageAuthorLabel-roleAssistant",
diff --git a/docs/pages/x/api/chat/chat-message-avatar.json b/docs/pages/x/api/chat/chat-message-avatar.json
index 6dd7f3b4ca567..241c4b8958d35 100644
--- a/docs/pages/x/api/chat/chat-message-avatar.json
+++ b/docs/pages/x/api/chat/chat-message-avatar.json
@@ -84,6 +84,12 @@
"description": "Styles applied to the message meta element.",
"isGlobal": false
},
+ {
+ "key": "noAvatar",
+ "className": "MuiChatMessageAvatar-noAvatar",
+ "description": "Applied when the avatar slot is hidden (`slots.avatar: null`) so the grid drops the avatar track.",
+ "isGlobal": false
+ },
{
"key": "roleAssistant",
"className": "MuiChatMessageAvatar-roleAssistant",
diff --git a/docs/pages/x/api/chat/chat-message-content.json b/docs/pages/x/api/chat/chat-message-content.json
index 5916c026a1125..e67928d155e69 100644
--- a/docs/pages/x/api/chat/chat-message-content.json
+++ b/docs/pages/x/api/chat/chat-message-content.json
@@ -92,6 +92,12 @@
"description": "Styles applied to the message meta element.",
"isGlobal": false
},
+ {
+ "key": "noAvatar",
+ "className": "MuiChatMessageContent-noAvatar",
+ "description": "Applied when the avatar slot is hidden (`slots.avatar: null`) so the grid drops the avatar track.",
+ "isGlobal": false
+ },
{
"key": "roleAssistant",
"className": "MuiChatMessageContent-roleAssistant",
diff --git a/docs/pages/x/api/chat/chat-message-error.js b/docs/pages/x/api/chat/chat-message-error.js
new file mode 100644
index 0000000000000..7a922e5a86803
--- /dev/null
+++ b/docs/pages/x/api/chat/chat-message-error.js
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import { ApiPage } from '@mui/internal-core-docs/ApiPage';
+import { mapApiPageTranslations } from '@mui/internal-core-docs/mapApiPageTranslations';
+import jsonPageContent from './chat-message-error.json';
+
+export default function Page(props) {
+ const { descriptions } = props;
+ return ;
+}
+
+export async function getStaticProps() {
+ const req = require.context(
+ 'docs/translations/api-docs/chat/chat-message-error',
+ false,
+ /\.\/chat-message-error.*\.json$/,
+ );
+ const descriptions = mapApiPageTranslations(req);
+
+ return { props: { descriptions } };
+}
diff --git a/docs/pages/x/api/chat/chat-message-error.json b/docs/pages/x/api/chat/chat-message-error.json
new file mode 100644
index 0000000000000..303a3ca4da464
--- /dev/null
+++ b/docs/pages/x/api/chat/chat-message-error.json
@@ -0,0 +1,35 @@
+{
+ "props": {},
+ "name": "ChatMessageError",
+ "imports": [
+ "import { ChatMessageError } from '@mui/x-chat/ChatMessageError';",
+ "import { ChatMessageError } from '@mui/x-chat';"
+ ],
+ "classes": [
+ {
+ "key": "message",
+ "className": "MuiChatMessageError-message",
+ "description": "Styles applied to the message text element.",
+ "isGlobal": false
+ },
+ {
+ "key": "retryButton",
+ "className": "MuiChatMessageError-retryButton",
+ "description": "Styles applied to the retry button.",
+ "isGlobal": false
+ },
+ {
+ "key": "root",
+ "className": "MuiChatMessageError-root",
+ "description": "Styles applied to the root element.",
+ "isGlobal": false
+ }
+ ],
+ "spread": true,
+ "themeDefaultProps": null,
+ "muiName": "MuiChatMessageError",
+ "filename": "/packages/x-chat/src/ChatMessageError/ChatMessageError.tsx",
+ "inheritance": null,
+ "demos": "
",
+ "cssComponent": false
+}
diff --git a/docs/pages/x/api/chat/chat-message-group.json b/docs/pages/x/api/chat/chat-message-group.json
index 2cb1aa276604b..cbf3752207d53 100644
--- a/docs/pages/x/api/chat/chat-message-group.json
+++ b/docs/pages/x/api/chat/chat-message-group.json
@@ -89,6 +89,12 @@
"description": "Styles applied to the message meta element.",
"isGlobal": false
},
+ {
+ "key": "noAvatar",
+ "className": "MuiChatMessageGroup-noAvatar",
+ "description": "Applied when the avatar slot is hidden (`slots.avatar: null`) so the grid drops the avatar track.",
+ "isGlobal": false
+ },
{
"key": "roleAssistant",
"className": "MuiChatMessageGroup-roleAssistant",
diff --git a/docs/pages/x/api/chat/chat-message-list.json b/docs/pages/x/api/chat/chat-message-list.json
index 60189bfe5ab7d..5098580b4beec 100644
--- a/docs/pages/x/api/chat/chat-message-list.json
+++ b/docs/pages/x/api/chat/chat-message-list.json
@@ -3,7 +3,8 @@
"autoScroll": {
"type": { "name": "union", "description": "{ buffer?: number } | bool" },
"default": "true"
- }
+ },
+ "renderItem": { "type": { "name": "func" } }
},
"name": "ChatMessageList",
"imports": [
diff --git a/docs/pages/x/api/chat/chat-message-meta.json b/docs/pages/x/api/chat/chat-message-meta.json
index 6c1f4de0d34a2..8f14f47382de1 100644
--- a/docs/pages/x/api/chat/chat-message-meta.json
+++ b/docs/pages/x/api/chat/chat-message-meta.json
@@ -84,6 +84,12 @@
"description": "Styles applied to the message meta element.",
"isGlobal": false
},
+ {
+ "key": "noAvatar",
+ "className": "MuiChatMessageMeta-noAvatar",
+ "description": "Applied when the avatar slot is hidden (`slots.avatar: null`) so the grid drops the avatar track.",
+ "isGlobal": false
+ },
{
"key": "roleAssistant",
"className": "MuiChatMessageMeta-roleAssistant",
diff --git a/docs/pages/x/api/chat/chat-root.json b/docs/pages/x/api/chat/chat-root.json
index 464de68732ea3..3ca04e0527522 100644
--- a/docs/pages/x/api/chat/chat-root.json
+++ b/docs/pages/x/api/chat/chat-root.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ChatRoot",
- "imports": [
- "import { ChatRoot } from '@mui/x-chat/headless';",
- "import { ChatRoot } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ChatRoot } from '@mui/x-chat/headless';"],
"slots": [{ "name": "root", "description": "", "class": null }],
"classes": [],
"spread": true,
diff --git a/docs/pages/x/api/chat/composer-attach-button.json b/docs/pages/x/api/chat/composer-attach-button.json
index 2466bf574a0ca..c965c2bb896c7 100644
--- a/docs/pages/x/api/chat/composer-attach-button.json
+++ b/docs/pages/x/api/chat/composer-attach-button.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ComposerAttachButton",
- "imports": [
- "import { ComposerAttachButton } from '@mui/x-chat/headless';",
- "import { ComposerAttachButton } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ComposerAttachButton } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "attachButton", "description": "", "class": null },
{ "name": "attachInput", "description": "", "class": null }
diff --git a/docs/pages/x/api/chat/composer-attachment-list.json b/docs/pages/x/api/chat/composer-attachment-list.json
index 64cd2611ffdb6..478c3b23d96d4 100644
--- a/docs/pages/x/api/chat/composer-attachment-list.json
+++ b/docs/pages/x/api/chat/composer-attachment-list.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ComposerAttachmentList",
- "imports": [
- "import { ComposerAttachmentList } from '@mui/x-chat/headless';",
- "import { ComposerAttachmentList } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ComposerAttachmentList } from '@mui/x-chat/headless';"],
"slots": [{ "name": "attachmentList", "description": "", "class": null }],
"classes": [],
"muiName": "MuiComposerAttachmentList",
diff --git a/docs/pages/x/api/chat/composer-helper-text.json b/docs/pages/x/api/chat/composer-helper-text.json
index e8ad93a0e27cb..21f6de630fded 100644
--- a/docs/pages/x/api/chat/composer-helper-text.json
+++ b/docs/pages/x/api/chat/composer-helper-text.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ComposerHelperText",
- "imports": [
- "import { ComposerHelperText } from '@mui/x-chat/headless';",
- "import { ComposerHelperText } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ComposerHelperText } from '@mui/x-chat/headless';"],
"slots": [{ "name": "helperText", "description": "", "class": null }],
"classes": [],
"muiName": "MuiComposerHelperText",
diff --git a/docs/pages/x/api/chat/composer-label.json b/docs/pages/x/api/chat/composer-label.json
index 83878aae0b66d..bc72084e82450 100644
--- a/docs/pages/x/api/chat/composer-label.json
+++ b/docs/pages/x/api/chat/composer-label.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ComposerLabel",
- "imports": [
- "import { ComposerLabel } from '@mui/x-chat/headless';",
- "import { ComposerLabel } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ComposerLabel } from '@mui/x-chat/headless';"],
"slots": [{ "name": "label", "description": "", "class": null }],
"classes": [],
"muiName": "MuiComposerLabel",
diff --git a/docs/pages/x/api/chat/composer-root.json b/docs/pages/x/api/chat/composer-root.json
index 58a093e91668f..5891c3458e532 100644
--- a/docs/pages/x/api/chat/composer-root.json
+++ b/docs/pages/x/api/chat/composer-root.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ComposerRoot",
- "imports": [
- "import { ComposerRoot } from '@mui/x-chat/headless';",
- "import { ComposerRoot } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ComposerRoot } from '@mui/x-chat/headless';"],
"slots": [{ "name": "root", "description": "", "class": null }],
"classes": [],
"spread": true,
diff --git a/docs/pages/x/api/chat/composer-send-button.json b/docs/pages/x/api/chat/composer-send-button.json
index 1e7a7c7edb497..0a1df48d5235e 100644
--- a/docs/pages/x/api/chat/composer-send-button.json
+++ b/docs/pages/x/api/chat/composer-send-button.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ComposerSendButton",
- "imports": [
- "import { ComposerSendButton } from '@mui/x-chat/headless';",
- "import { ComposerSendButton } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ComposerSendButton } from '@mui/x-chat/headless';"],
"slots": [{ "name": "sendButton", "description": "", "class": null }],
"classes": [],
"muiName": "MuiComposerSendButton",
diff --git a/docs/pages/x/api/chat/composer-text-area.json b/docs/pages/x/api/chat/composer-text-area.json
index 6a8e0dfb8c9d9..b34ad359419fe 100644
--- a/docs/pages/x/api/chat/composer-text-area.json
+++ b/docs/pages/x/api/chat/composer-text-area.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ComposerTextArea",
- "imports": [
- "import { ComposerTextArea } from '@mui/x-chat/headless';",
- "import { ComposerTextArea } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ComposerTextArea } from '@mui/x-chat/headless';"],
"slots": [{ "name": "input", "description": "", "class": null }],
"classes": [],
"muiName": "MuiComposerTextArea",
diff --git a/docs/pages/x/api/chat/composer-toolbar.json b/docs/pages/x/api/chat/composer-toolbar.json
index e16077b187f18..300c70fa9dd82 100644
--- a/docs/pages/x/api/chat/composer-toolbar.json
+++ b/docs/pages/x/api/chat/composer-toolbar.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ComposerToolbar",
- "imports": [
- "import { ComposerToolbar } from '@mui/x-chat/headless';",
- "import { ComposerToolbar } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ComposerToolbar } from '@mui/x-chat/headless';"],
"slots": [{ "name": "toolbar", "description": "", "class": null }],
"classes": [],
"muiName": "MuiComposerToolbar",
diff --git a/docs/pages/x/api/chat/conversation-header-actions.json b/docs/pages/x/api/chat/conversation-header-actions.json
index fbfa4a81024d7..bf79dbc6485da 100644
--- a/docs/pages/x/api/chat/conversation-header-actions.json
+++ b/docs/pages/x/api/chat/conversation-header-actions.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationHeaderActions",
- "imports": [
- "import { ConversationHeaderActions } from '@mui/x-chat/headless';",
- "import { ConversationHeaderActions } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationHeaderActions } from '@mui/x-chat/headless';"],
"slots": [{ "name": "actions", "description": "", "class": null }],
"classes": [],
"muiName": "MuiConversationHeaderActions",
diff --git a/docs/pages/x/api/chat/conversation-header.json b/docs/pages/x/api/chat/conversation-header.json
index 3ae09c616ac91..182a33706044f 100644
--- a/docs/pages/x/api/chat/conversation-header.json
+++ b/docs/pages/x/api/chat/conversation-header.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationHeader",
- "imports": [
- "import { ConversationHeader } from '@mui/x-chat/headless';",
- "import { ConversationHeader } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationHeader } from '@mui/x-chat/headless';"],
"slots": [{ "name": "header", "description": "", "class": null }],
"classes": [],
"muiName": "MuiConversationHeader",
diff --git a/docs/pages/x/api/chat/conversation-list-item-avatar.json b/docs/pages/x/api/chat/conversation-list-item-avatar.json
index 010e555f108ad..c935ec422e791 100644
--- a/docs/pages/x/api/chat/conversation-list-item-avatar.json
+++ b/docs/pages/x/api/chat/conversation-list-item-avatar.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationListItemAvatar",
- "imports": [
- "import { ConversationListItemAvatar } from '@mui/x-chat/headless';",
- "import { ConversationListItemAvatar } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationListItemAvatar } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "root", "description": "", "class": null },
{ "name": "image", "description": "", "class": null }
diff --git a/docs/pages/x/api/chat/conversation-list-item.json b/docs/pages/x/api/chat/conversation-list-item.json
index 50706a61a31dc..bbc539208cb94 100644
--- a/docs/pages/x/api/chat/conversation-list-item.json
+++ b/docs/pages/x/api/chat/conversation-list-item.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationListItem",
- "imports": [
- "import { ConversationListItem } from '@mui/x-chat/headless';",
- "import { ConversationListItem } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationListItem } from '@mui/x-chat/headless';"],
"slots": [{ "name": "root", "description": "", "class": null }],
"classes": [],
"muiName": "MuiConversationListItem",
diff --git a/docs/pages/x/api/chat/conversation-list-preview.json b/docs/pages/x/api/chat/conversation-list-preview.json
index 398eacc63a949..b72edbf0503e4 100644
--- a/docs/pages/x/api/chat/conversation-list-preview.json
+++ b/docs/pages/x/api/chat/conversation-list-preview.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationListPreview",
- "imports": [
- "import { ConversationListPreview } from '@mui/x-chat/headless';",
- "import { ConversationListPreview } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationListPreview } from '@mui/x-chat/headless';"],
"slots": [{ "name": "root", "description": "", "class": null }],
"classes": [],
"muiName": "MuiConversationListPreview",
diff --git a/docs/pages/x/api/chat/conversation-list-root.json b/docs/pages/x/api/chat/conversation-list-root.json
index 5384ecc613f0f..9f965a61ac85c 100644
--- a/docs/pages/x/api/chat/conversation-list-root.json
+++ b/docs/pages/x/api/chat/conversation-list-root.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationListRoot",
- "imports": [
- "import { ConversationListRoot } from '@mui/x-chat/headless';",
- "import { ConversationListRoot } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationListRoot } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "root", "description": "", "class": null },
{ "name": "scroller", "description": "", "class": null },
diff --git a/docs/pages/x/api/chat/conversation-list-timestamp.json b/docs/pages/x/api/chat/conversation-list-timestamp.json
index 61dfa84816864..0a5dfa6a4196e 100644
--- a/docs/pages/x/api/chat/conversation-list-timestamp.json
+++ b/docs/pages/x/api/chat/conversation-list-timestamp.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationListTimestamp",
- "imports": [
- "import { ConversationListTimestamp } from '@mui/x-chat/headless';",
- "import { ConversationListTimestamp } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationListTimestamp } from '@mui/x-chat/headless';"],
"slots": [{ "name": "root", "description": "", "class": null }],
"classes": [],
"muiName": "MuiConversationListTimestamp",
diff --git a/docs/pages/x/api/chat/conversation-list-title.json b/docs/pages/x/api/chat/conversation-list-title.json
index 99061daed53b2..cdd1e7528fb12 100644
--- a/docs/pages/x/api/chat/conversation-list-title.json
+++ b/docs/pages/x/api/chat/conversation-list-title.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationListTitle",
- "imports": [
- "import { ConversationListTitle } from '@mui/x-chat/headless';",
- "import { ConversationListTitle } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationListTitle } from '@mui/x-chat/headless';"],
"slots": [{ "name": "root", "description": "", "class": null }],
"classes": [],
"muiName": "MuiConversationListTitle",
diff --git a/docs/pages/x/api/chat/conversation-list-unread-badge.json b/docs/pages/x/api/chat/conversation-list-unread-badge.json
index c78c610000432..ad72cb8484f99 100644
--- a/docs/pages/x/api/chat/conversation-list-unread-badge.json
+++ b/docs/pages/x/api/chat/conversation-list-unread-badge.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationListUnreadBadge",
- "imports": [
- "import { ConversationListUnreadBadge } from '@mui/x-chat/headless';",
- "import { ConversationListUnreadBadge } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationListUnreadBadge } from '@mui/x-chat/headless';"],
"slots": [{ "name": "root", "description": "", "class": null }],
"classes": [],
"muiName": "MuiConversationListUnreadBadge",
diff --git a/docs/pages/x/api/chat/conversation-root.json b/docs/pages/x/api/chat/conversation-root.json
index 89b0088ef27f1..a41fd073895fd 100644
--- a/docs/pages/x/api/chat/conversation-root.json
+++ b/docs/pages/x/api/chat/conversation-root.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationRoot",
- "imports": [
- "import { ConversationRoot } from '@mui/x-chat/headless';",
- "import { ConversationRoot } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationRoot } from '@mui/x-chat/headless';"],
"slots": [{ "name": "root", "description": "", "class": null }],
"classes": [],
"spread": true,
diff --git a/docs/pages/x/api/chat/conversation-subtitle.json b/docs/pages/x/api/chat/conversation-subtitle.json
index 5309a6b0c09f3..e535ddb1f5b75 100644
--- a/docs/pages/x/api/chat/conversation-subtitle.json
+++ b/docs/pages/x/api/chat/conversation-subtitle.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationSubtitle",
- "imports": [
- "import { ConversationSubtitle } from '@mui/x-chat/headless';",
- "import { ConversationSubtitle } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationSubtitle } from '@mui/x-chat/headless';"],
"slots": [{ "name": "subtitle", "description": "", "class": null }],
"classes": [],
"muiName": "MuiConversationSubtitle",
diff --git a/docs/pages/x/api/chat/conversation-title.json b/docs/pages/x/api/chat/conversation-title.json
index dd3c0537b72e1..6a00db7b6307e 100644
--- a/docs/pages/x/api/chat/conversation-title.json
+++ b/docs/pages/x/api/chat/conversation-title.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ConversationTitle",
- "imports": [
- "import { ConversationTitle } from '@mui/x-chat/headless';",
- "import { ConversationTitle } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ConversationTitle } from '@mui/x-chat/headless';"],
"slots": [{ "name": "title", "description": "", "class": null }],
"classes": [],
"muiName": "MuiConversationTitle",
diff --git a/docs/pages/x/api/chat/message-actions.json b/docs/pages/x/api/chat/message-actions.json
index 6ed07a6600b9d..0af0e3a8322e0 100644
--- a/docs/pages/x/api/chat/message-actions.json
+++ b/docs/pages/x/api/chat/message-actions.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "MessageActions",
- "imports": [
- "import { MessageActions } from '@mui/x-chat/headless';",
- "import { MessageActions } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { MessageActions } from '@mui/x-chat/headless';"],
"slots": [{ "name": "actions", "description": "", "class": null }],
"classes": [],
"muiName": "MuiMessageActions",
diff --git a/docs/pages/x/api/chat/message-author-label.json b/docs/pages/x/api/chat/message-author-label.json
index b5662b2a21242..048a9a28a2892 100644
--- a/docs/pages/x/api/chat/message-author-label.json
+++ b/docs/pages/x/api/chat/message-author-label.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "MessageAuthorLabel",
- "imports": [
- "import { MessageAuthorLabel } from '@mui/x-chat/headless';",
- "import { MessageAuthorLabel } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { MessageAuthorLabel } from '@mui/x-chat/headless';"],
"slots": [{ "name": "authorLabel", "description": "", "class": null }],
"classes": [],
"muiName": "MuiMessageAuthorLabel",
diff --git a/docs/pages/x/api/chat/message-avatar.json b/docs/pages/x/api/chat/message-avatar.json
index 8bfbfa8c1f9bc..6d8a8a45439f6 100644
--- a/docs/pages/x/api/chat/message-avatar.json
+++ b/docs/pages/x/api/chat/message-avatar.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "MessageAvatar",
- "imports": [
- "import { MessageAvatar } from '@mui/x-chat/headless';",
- "import { MessageAvatar } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { MessageAvatar } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "avatar", "description": "", "class": null },
{ "name": "image", "description": "", "class": null }
diff --git a/docs/pages/x/api/chat/message-content.json b/docs/pages/x/api/chat/message-content.json
index 9c51797be4e60..e7f7f76a7c07d 100644
--- a/docs/pages/x/api/chat/message-content.json
+++ b/docs/pages/x/api/chat/message-content.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "MessageContent",
- "imports": [
- "import { MessageContent } from '@mui/x-chat/headless';",
- "import { MessageContent } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { MessageContent } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "content", "description": "", "class": null },
{ "name": "bubble", "description": "", "class": null }
diff --git a/docs/pages/x/api/chat/message-error.json b/docs/pages/x/api/chat/message-error.json
index fbf0e50c4bb31..01ebb043a30a7 100644
--- a/docs/pages/x/api/chat/message-error.json
+++ b/docs/pages/x/api/chat/message-error.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "MessageError",
- "imports": [
- "import { MessageError } from '@mui/x-chat/headless';",
- "import { MessageError } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { MessageError } from '@mui/x-chat/headless';"],
"slots": [
{
"name": "root",
diff --git a/docs/pages/x/api/chat/message-group.json b/docs/pages/x/api/chat/message-group.json
index bf9bf23d673f2..ded81f3d58572 100644
--- a/docs/pages/x/api/chat/message-group.json
+++ b/docs/pages/x/api/chat/message-group.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "MessageGroup",
- "imports": [
- "import { MessageGroup } from '@mui/x-chat/headless';",
- "import { MessageGroup } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { MessageGroup } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "group", "description": "", "class": null },
{ "name": "authorName", "description": "", "class": null },
diff --git a/docs/pages/x/api/chat/message-list-date-divider.json b/docs/pages/x/api/chat/message-list-date-divider.json
index 4a8ad547381a3..21815908ac841 100644
--- a/docs/pages/x/api/chat/message-list-date-divider.json
+++ b/docs/pages/x/api/chat/message-list-date-divider.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "MessageListDateDivider",
- "imports": [
- "import { MessageListDateDivider } from '@mui/x-chat/headless';",
- "import { MessageListDateDivider } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { MessageListDateDivider } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "divider", "description": "", "class": null },
{ "name": "line", "description": "", "class": null },
diff --git a/docs/pages/x/api/chat/message-list-root.json b/docs/pages/x/api/chat/message-list-root.json
index 776c3f5c4bca4..5619464afb401 100644
--- a/docs/pages/x/api/chat/message-list-root.json
+++ b/docs/pages/x/api/chat/message-list-root.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "MessageListRoot",
- "imports": [
- "import { MessageListRoot } from '@mui/x-chat/headless';",
- "import { MessageListRoot } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { MessageListRoot } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "messageList", "description": "", "class": null },
{ "name": "messageListScroller", "description": "", "class": null },
diff --git a/docs/pages/x/api/chat/message-meta.json b/docs/pages/x/api/chat/message-meta.json
index 42d88ace297bf..8486eaa456244 100644
--- a/docs/pages/x/api/chat/message-meta.json
+++ b/docs/pages/x/api/chat/message-meta.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "MessageMeta",
- "imports": [
- "import { MessageMeta } from '@mui/x-chat/headless';",
- "import { MessageMeta } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { MessageMeta } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "meta", "description": "", "class": null },
{ "name": "timestamp", "description": "", "class": null },
diff --git a/docs/pages/x/api/chat/message-root.json b/docs/pages/x/api/chat/message-root.json
index 9c475410921ee..10a9fb0d26103 100644
--- a/docs/pages/x/api/chat/message-root.json
+++ b/docs/pages/x/api/chat/message-root.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "MessageRoot",
- "imports": [
- "import { MessageRoot } from '@mui/x-chat/headless';",
- "import { MessageRoot } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { MessageRoot } from '@mui/x-chat/headless';"],
"slots": [{ "name": "root", "description": "", "class": null }],
"classes": [],
"spread": true,
diff --git a/docs/pages/x/api/chat/scroll-to-bottom-affordance.json b/docs/pages/x/api/chat/scroll-to-bottom-affordance.json
index aa31c292de12d..52082816caf1c 100644
--- a/docs/pages/x/api/chat/scroll-to-bottom-affordance.json
+++ b/docs/pages/x/api/chat/scroll-to-bottom-affordance.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "ScrollToBottomAffordance",
- "imports": [
- "import { ScrollToBottomAffordance } from '@mui/x-chat/headless';",
- "import { ScrollToBottomAffordance } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { ScrollToBottomAffordance } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "root", "description": "", "class": null },
{ "name": "badge", "description": "", "class": null },
diff --git a/docs/pages/x/api/chat/typing-indicator.json b/docs/pages/x/api/chat/typing-indicator.json
index 6ce9f3e7ee580..891578346b359 100644
--- a/docs/pages/x/api/chat/typing-indicator.json
+++ b/docs/pages/x/api/chat/typing-indicator.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "TypingIndicator",
- "imports": [
- "import { TypingIndicator } from '@mui/x-chat/headless';",
- "import { TypingIndicator } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { TypingIndicator } from '@mui/x-chat/headless';"],
"slots": [{ "name": "root", "description": "", "class": null }],
"classes": [],
"muiName": "MuiTypingIndicator",
diff --git a/docs/pages/x/api/chat/unread-marker.json b/docs/pages/x/api/chat/unread-marker.json
index 32a556f18fb3f..095a2f4c83b6a 100644
--- a/docs/pages/x/api/chat/unread-marker.json
+++ b/docs/pages/x/api/chat/unread-marker.json
@@ -1,10 +1,7 @@
{
"props": {},
"name": "UnreadMarker",
- "imports": [
- "import { UnreadMarker } from '@mui/x-chat/headless';",
- "import { UnreadMarker } from '@mui/x-chat/headless';"
- ],
+ "imports": ["import { UnreadMarker } from '@mui/x-chat/headless';"],
"slots": [
{ "name": "root", "description": "", "class": null },
{ "name": "label", "description": "", "class": null }
diff --git a/docs/translations/api-docs/chat/chat-date-divider/chat-date-divider.json b/docs/translations/api-docs/chat/chat-date-divider/chat-date-divider.json
index c31e9bf466441..ea1c6ecb18c44 100644
--- a/docs/translations/api-docs/chat/chat-date-divider/chat-date-divider.json
+++ b/docs/translations/api-docs/chat/chat-date-divider/chat-date-divider.json
@@ -47,6 +47,9 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the message meta element"
},
+ "noAvatar": {
+ "description": "Applied when the avatar slot is hidden (slots.avatar: null) so the grid drops the avatar track."
+ },
"roleAssistant": { "description": "Applied when the message role is 'assistant'" },
"roleUser": { "description": "Applied when the message role is 'user'" },
"root": {
diff --git a/docs/translations/api-docs/chat/chat-message-actions/chat-message-actions.json b/docs/translations/api-docs/chat/chat-message-actions/chat-message-actions.json
index c31e9bf466441..ea1c6ecb18c44 100644
--- a/docs/translations/api-docs/chat/chat-message-actions/chat-message-actions.json
+++ b/docs/translations/api-docs/chat/chat-message-actions/chat-message-actions.json
@@ -47,6 +47,9 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the message meta element"
},
+ "noAvatar": {
+ "description": "Applied when the avatar slot is hidden (slots.avatar: null) so the grid drops the avatar track."
+ },
"roleAssistant": { "description": "Applied when the message role is 'assistant'" },
"roleUser": { "description": "Applied when the message role is 'user'" },
"root": {
diff --git a/docs/translations/api-docs/chat/chat-message-author-label/chat-message-author-label.json b/docs/translations/api-docs/chat/chat-message-author-label/chat-message-author-label.json
index c31e9bf466441..ea1c6ecb18c44 100644
--- a/docs/translations/api-docs/chat/chat-message-author-label/chat-message-author-label.json
+++ b/docs/translations/api-docs/chat/chat-message-author-label/chat-message-author-label.json
@@ -47,6 +47,9 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the message meta element"
},
+ "noAvatar": {
+ "description": "Applied when the avatar slot is hidden (slots.avatar: null) so the grid drops the avatar track."
+ },
"roleAssistant": { "description": "Applied when the message role is 'assistant'" },
"roleUser": { "description": "Applied when the message role is 'user'" },
"root": {
diff --git a/docs/translations/api-docs/chat/chat-message-avatar/chat-message-avatar.json b/docs/translations/api-docs/chat/chat-message-avatar/chat-message-avatar.json
index c31e9bf466441..ea1c6ecb18c44 100644
--- a/docs/translations/api-docs/chat/chat-message-avatar/chat-message-avatar.json
+++ b/docs/translations/api-docs/chat/chat-message-avatar/chat-message-avatar.json
@@ -47,6 +47,9 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the message meta element"
},
+ "noAvatar": {
+ "description": "Applied when the avatar slot is hidden (slots.avatar: null) so the grid drops the avatar track."
+ },
"roleAssistant": { "description": "Applied when the message role is 'assistant'" },
"roleUser": { "description": "Applied when the message role is 'user'" },
"root": {
diff --git a/docs/translations/api-docs/chat/chat-message-content/chat-message-content.json b/docs/translations/api-docs/chat/chat-message-content/chat-message-content.json
index fd3b4fea74905..68578a1b0a903 100644
--- a/docs/translations/api-docs/chat/chat-message-content/chat-message-content.json
+++ b/docs/translations/api-docs/chat/chat-message-content/chat-message-content.json
@@ -54,6 +54,9 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the message meta element"
},
+ "noAvatar": {
+ "description": "Applied when the avatar slot is hidden (slots.avatar: null) so the grid drops the avatar track."
+ },
"roleAssistant": { "description": "Applied when the message role is 'assistant'" },
"roleUser": { "description": "Applied when the message role is 'user'" },
"root": {
diff --git a/docs/translations/api-docs/chat/chat-message-error/chat-message-error.json b/docs/translations/api-docs/chat/chat-message-error/chat-message-error.json
new file mode 100644
index 0000000000000..32127d46dc591
--- /dev/null
+++ b/docs/translations/api-docs/chat/chat-message-error/chat-message-error.json
@@ -0,0 +1,15 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {},
+ "classDescriptions": {
+ "message": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the message text element"
+ },
+ "retryButton": {
+ "description": "Styles applied to {{nodeName}}.",
+ "nodeName": "the retry button"
+ },
+ "root": { "description": "Styles applied to the root element." }
+ }
+}
diff --git a/docs/translations/api-docs/chat/chat-message-group/chat-message-group.json b/docs/translations/api-docs/chat/chat-message-group/chat-message-group.json
index 86e92880b17d9..fa9a0f27505af 100644
--- a/docs/translations/api-docs/chat/chat-message-group/chat-message-group.json
+++ b/docs/translations/api-docs/chat/chat-message-group/chat-message-group.json
@@ -51,6 +51,9 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the message meta element"
},
+ "noAvatar": {
+ "description": "Applied when the avatar slot is hidden (slots.avatar: null) so the grid drops the avatar track."
+ },
"roleAssistant": { "description": "Applied when the message role is 'assistant'" },
"roleUser": { "description": "Applied when the message role is 'user'" },
"root": {
diff --git a/docs/translations/api-docs/chat/chat-message-list/chat-message-list.json b/docs/translations/api-docs/chat/chat-message-list/chat-message-list.json
index 2d1ce14d76f44..2c02ad2a87b0e 100644
--- a/docs/translations/api-docs/chat/chat-message-list/chat-message-list.json
+++ b/docs/translations/api-docs/chat/chat-message-list/chat-message-list.json
@@ -3,6 +3,9 @@
"propDescriptions": {
"autoScroll": {
"description": "Controls automatic scrolling to the bottom when new messages arrive or\nstreaming content grows, as long as the user is within buffer pixels of\nthe bottom.
\n\ntrue – enable with the default buffer (150 px). \n{ buffer: number } – enable with a custom threshold. \nfalse – disable (the scroll-to-bottom affordance is still available). \n \nScrolling when the user sends a message is always active.
\n"
+ },
+ "renderItem": {
+ "description": "Render a custom row for each message. When omitted, the default row used by ChatBox is rendered (group → message → avatar → content, with conditional inline meta, external meta in compact variant, and message actions slot). Provide a function to fully replace the row; use slot overrides to tweak individual sub-components without losing the default chrome."
}
},
"classDescriptions": {
diff --git a/docs/translations/api-docs/chat/chat-message-meta/chat-message-meta.json b/docs/translations/api-docs/chat/chat-message-meta/chat-message-meta.json
index c31e9bf466441..ea1c6ecb18c44 100644
--- a/docs/translations/api-docs/chat/chat-message-meta/chat-message-meta.json
+++ b/docs/translations/api-docs/chat/chat-message-meta/chat-message-meta.json
@@ -47,6 +47,9 @@
"description": "Styles applied to {{nodeName}}.",
"nodeName": "the message meta element"
},
+ "noAvatar": {
+ "description": "Applied when the avatar slot is hidden (slots.avatar: null) so the grid drops the avatar track."
+ },
"roleAssistant": { "description": "Applied when the message role is 'assistant'" },
"roleUser": { "description": "Applied when the message role is 'user'" },
"root": {
diff --git a/packages/x-chat/src/ChatBox/ChatBox.types.ts b/packages/x-chat/src/ChatBox/ChatBox.types.ts
index 9152e3ffccae8..1f89964929bfe 100644
--- a/packages/x-chat/src/ChatBox/ChatBox.types.ts
+++ b/packages/x-chat/src/ChatBox/ChatBox.types.ts
@@ -23,6 +23,7 @@ import type { ChatMessageContentProps } from '../ChatMessage/ChatMessageContent'
import type { ChatMessageMetaProps } from '../ChatMessage/ChatMessageMeta';
import type { ChatMessageActionsProps } from '../ChatMessage/ChatMessageActions';
import type { ChatMessageGroupProps } from '../ChatMessage/ChatMessageGroup';
+import type { ChatMessageErrorProps } from '../ChatMessageError/ChatMessageError';
import type { ChatDateDividerProps } from '../ChatMessage/ChatDateDivider';
import type { ChatComposerProps } from '../ChatComposer/ChatComposer';
import type { ChatComposerTextAreaProps } from '../ChatComposer/ChatComposerTextArea';
@@ -130,6 +131,48 @@ export interface ChatBoxSlotProps {
suggestions?: Partial;
}
+/**
+ * Slot map for the `messagesList` family — list-level chrome.
+ *
+ * `group` overrides the per-message group wrapper component.
+ */
+export interface ChatBoxMessagesListSlots {
+ group?: React.ElementType;
+ dateDivider?: React.ElementType;
+ unreadMarker?: React.ElementType;
+}
+
+export interface ChatBoxMessagesListSlotProps {
+ group?: Partial;
+ dateDivider?: Partial;
+ unreadMarker?: Partial;
+}
+
+/**
+ * Slot map for the `message` family — one row's parts.
+ */
+export interface ChatBoxMessageSlots {
+ root?: React.ElementType;
+ avatar?: React.ElementType | null;
+ content?: React.ElementType;
+ meta?: React.ElementType | null;
+ inlineMeta?: React.ElementType | null;
+ error?: React.ElementType;
+ actions?: React.ElementType | null;
+ authorName?: React.ElementType | null;
+}
+
+export interface ChatBoxMessageSlotProps {
+ root?: Partial;
+ avatar?: Partial;
+ content?: Partial;
+ meta?: Partial;
+ inlineMeta?: Record;
+ error?: Partial;
+ actions?: Partial;
+ authorName?: Record;
+}
+
export interface ChatBoxFeatures {
/**
* Whether to show the scroll-to-bottom affordance button when the user has scrolled up.
diff --git a/packages/x-chat/src/ChatCodeBlock/ChatCodeBlock.tsx b/packages/x-chat/src/ChatCodeBlock/ChatCodeBlock.tsx
index 9530dd2952675..1f0cfa7c58cae 100644
--- a/packages/x-chat/src/ChatCodeBlock/ChatCodeBlock.tsx
+++ b/packages/x-chat/src/ChatCodeBlock/ChatCodeBlock.tsx
@@ -2,7 +2,9 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
+import { SxProps, Theme } from '@mui/system';
import { styled, createUseThemeProps } from '../internals/zero-styled';
+import { useCopyToClipboard } from '../internals/useCopyToClipboard';
import { useChatCodeBlockUtilityClasses, type ChatCodeBlockClasses } from './chatCodeBlockClasses';
export interface ChatCodeBlockProps {
@@ -23,6 +25,7 @@ export interface ChatCodeBlockProps {
*/
highlighter?: (code: string, language: string) => React.ReactNode;
className?: string;
+ sx?: SxProps;
classes?: Partial;
}
@@ -136,38 +139,24 @@ const ChatCodeBlockCode = styled('code', {
const ChatCodeBlock = React.forwardRef(
function ChatCodeBlock(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiChatCodeBlock' });
- const { children, language, highlighter, className, classes: classesProp, ...other } = props;
+ const {
+ children,
+ language,
+ highlighter,
+ className,
+ classes: classesProp,
+ sx,
+ ...other
+ } = props;
const classes = useChatCodeBlockUtilityClasses(classesProp);
- const [copyState, setCopyState] = React.useState<'idle' | 'copied'>('idle');
- const resetTimerRef = React.useRef | null>(null);
-
- React.useEffect(() => {
- return () => {
- if (resetTimerRef.current !== null) {
- clearTimeout(resetTimerRef.current);
- }
- };
- }, []);
-
- const handleCopy = () => {
- navigator.clipboard.writeText(children).then(
- () => {
- setCopyState('copied');
- if (resetTimerRef.current !== null) {
- clearTimeout(resetTimerRef.current);
- }
- resetTimerRef.current = setTimeout(() => setCopyState('idle'), 2000);
- },
- () => {
- // Clipboard write failed (e.g. permissions denied) — no-op
- },
- );
- };
+ const { copyState, copy } = useCopyToClipboard();
+
+ const handleCopy = () => copy(children);
return (
-
+
{language ?? ''}
@@ -214,6 +203,11 @@ ChatCodeBlock.propTypes = {
* Language identifier shown in the header (e.g. "typescript", "python").
*/
language: PropTypes.string,
+ sx: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
+ PropTypes.func,
+ PropTypes.object,
+ ]),
} as any;
export { ChatCodeBlock };
diff --git a/packages/x-chat/src/ChatComposer/ChatComposer.test.tsx b/packages/x-chat/src/ChatComposer/ChatComposer.test.tsx
index f9903b7be86c0..7b9d0834c5b0c 100644
--- a/packages/x-chat/src/ChatComposer/ChatComposer.test.tsx
+++ b/packages/x-chat/src/ChatComposer/ChatComposer.test.tsx
@@ -31,7 +31,7 @@ describe('ChatComposer', () => {
expect(document.querySelector('.MuiChatComposer-root')).not.toBe(null);
});
- it('forwards custom className via slotProps.composerRoot', () => {
+ it('forwards custom className via slotProps.composer', () => {
render(
{
expect(document.querySelector('.MuiChatComposer-attachButton')).not.toBe(null);
});
- it('forwards custom className via slotProps.composerAttachButton', () => {
+ it('forwards custom className via slotProps.attach', () => {
render(
diff --git a/packages/x-chat/src/ChatComposer/ChatComposerSendButton.test.tsx b/packages/x-chat/src/ChatComposer/ChatComposerSendButton.test.tsx
index d296c229f34e9..115d9f43b11c3 100644
--- a/packages/x-chat/src/ChatComposer/ChatComposerSendButton.test.tsx
+++ b/packages/x-chat/src/ChatComposer/ChatComposerSendButton.test.tsx
@@ -30,7 +30,7 @@ describe('ChatComposerSendButton', () => {
expect(document.querySelector('.MuiChatComposer-sendButton')).not.toBe(null);
});
- it('forwards custom className via slotProps.composerSendButton', () => {
+ it('forwards custom className via slotProps.send', () => {
render(
{
expect(document.querySelector('.MuiChatComposer-textArea')).not.toBe(null);
});
- it('forwards custom className via slotProps.composerInput', () => {
+ it('forwards custom className via slotProps.input', () => {
render(
styles.root,
})(({ theme }) => ({
+ // Default inline-sidebar width token. Consumed by the scroller below and by the
+ // narrow-layout Drawer in ChatBoxContent; override it to resize the conversation list.
'--ChatBox-conversationListWidth': '260px',
display: 'flex',
flexDirection: 'column',
@@ -65,6 +78,9 @@ const ChatConversationListScrollerStyled = styled('div', {
height: '100%',
overflow: 'hidden',
flexShrink: 0,
+ // Anonymous container query: ChatBox sets `containerType: 'inline-size'` without a
+ // `containerName`, so this matches the nearest size container (the ChatBox root) and
+ // hides the inline sidebar on narrow layouts. Mirrors `ChatBoxContent`'s JS observer.
'@container (max-width: 599.95px)': {
display: 'none',
},
@@ -305,80 +321,321 @@ const ChatConversationListItemActionsRoot = styled('div', {
// styled root so MUI theming is applied.
// ---------------------------------------------------------------------------
-const ChatConversationListItemAvatarStyled = React.forwardRef(
- function ChatConversationListItemAvatarStyled(props: any, ref) {
- return (
-
- );
+// Wrapper around the styled item root that satisfies
+// `React.JSXElementConstructor`. The styled root
+// uses `shouldForwardProp` to filter out the conversation-related props at
+// runtime, so we just spread them through.
+const ChatConversationListItemSlot = React.forwardRef(
+ function ChatConversationListItemSlot(props, ref) {
+ return ;
},
);
-const ChatConversationListItemContentStyled = React.forwardRef(
- function ChatConversationListItemContentStyled(props: any, ref) {
- return (
-
- );
- },
-);
+ChatConversationListItemSlot.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ children: PropTypes.node,
+ conversation: PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ lastMessageAt: PropTypes.string,
+ metadata: PropTypes.object,
+ participants: PropTypes.arrayOf(
+ PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ displayName: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ isOnline: PropTypes.bool,
+ metadata: PropTypes.object,
+ role: PropTypes.oneOf(['assistant', 'system', 'user']),
+ }),
+ ),
+ readState: PropTypes.oneOf(['read', 'unread']),
+ subtitle: PropTypes.string,
+ title: PropTypes.string,
+ unreadCount: PropTypes.number,
+ }).isRequired,
+ focused: PropTypes.bool,
+ selected: PropTypes.bool,
+ slotProps: PropTypes.object,
+ slots: PropTypes.object,
+ unread: PropTypes.bool,
+} as any;
-const ChatConversationListTitleStyled = React.forwardRef(
- function ChatConversationListTitleStyled(props: any, ref) {
- return (
-
- );
- },
-);
+const ChatConversationListItemAvatarStyled = React.forwardRef<
+ HTMLDivElement,
+ ConversationListItemAvatarProps
+>(function ChatConversationListItemAvatarStyled(props, ref) {
+ return (
+
+ );
+});
-const ChatConversationListPreviewStyled = React.forwardRef(
- function ChatConversationListPreviewStyled(props: any, ref) {
- return (
-
- );
- },
-);
+ChatConversationListItemAvatarStyled.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ conversation: PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ lastMessageAt: PropTypes.string,
+ metadata: PropTypes.object,
+ participants: PropTypes.arrayOf(
+ PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ displayName: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ isOnline: PropTypes.bool,
+ metadata: PropTypes.object,
+ role: PropTypes.oneOf(['assistant', 'system', 'user']),
+ }),
+ ),
+ readState: PropTypes.oneOf(['read', 'unread']),
+ subtitle: PropTypes.string,
+ title: PropTypes.string,
+ unreadCount: PropTypes.number,
+ }).isRequired,
+ focused: PropTypes.bool,
+ selected: PropTypes.bool,
+ slotProps: PropTypes.object,
+ slots: PropTypes.object,
+ unread: PropTypes.bool,
+} as any;
-const ChatConversationListTimestampStyled = React.forwardRef(
- function ChatConversationListTimestampStyled(props: any, ref) {
- return (
-
- );
- },
-);
+const ChatConversationListItemContentStyled = React.forwardRef<
+ HTMLDivElement,
+ ConversationListItemContentProps
+>(function ChatConversationListItemContentStyled(props, ref) {
+ return (
+
+ );
+});
-const ChatConversationListUnreadBadgeStyled = React.forwardRef(
- function ChatConversationListUnreadBadgeStyled(props: any, ref) {
- return (
-
- );
- },
-);
+ChatConversationListItemContentStyled.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ conversation: PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ lastMessageAt: PropTypes.string,
+ metadata: PropTypes.object,
+ participants: PropTypes.arrayOf(
+ PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ displayName: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ isOnline: PropTypes.bool,
+ metadata: PropTypes.object,
+ role: PropTypes.oneOf(['assistant', 'system', 'user']),
+ }),
+ ),
+ readState: PropTypes.oneOf(['read', 'unread']),
+ subtitle: PropTypes.string,
+ title: PropTypes.string,
+ unreadCount: PropTypes.number,
+ }).isRequired,
+ focused: PropTypes.bool,
+ selected: PropTypes.bool,
+ slotProps: PropTypes.object,
+ slots: PropTypes.object,
+ unread: PropTypes.bool,
+} as any;
+
+const ChatConversationListTitleStyled = React.forwardRef<
+ HTMLDivElement,
+ ConversationListTitleProps
+>(function ChatConversationListTitleStyled(props, ref) {
+ return (
+
+ );
+});
+
+ChatConversationListTitleStyled.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ conversation: PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ lastMessageAt: PropTypes.string,
+ metadata: PropTypes.object,
+ participants: PropTypes.arrayOf(
+ PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ displayName: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ isOnline: PropTypes.bool,
+ metadata: PropTypes.object,
+ role: PropTypes.oneOf(['assistant', 'system', 'user']),
+ }),
+ ),
+ readState: PropTypes.oneOf(['read', 'unread']),
+ subtitle: PropTypes.string,
+ title: PropTypes.string,
+ unreadCount: PropTypes.number,
+ }).isRequired,
+ focused: PropTypes.bool,
+ selected: PropTypes.bool,
+ slotProps: PropTypes.object,
+ slots: PropTypes.object,
+ unread: PropTypes.bool,
+} as any;
+
+const ChatConversationListPreviewStyled = React.forwardRef<
+ HTMLDivElement,
+ ConversationListPreviewProps
+>(function ChatConversationListPreviewStyled(props, ref) {
+ return (
+
+ );
+});
+
+ChatConversationListPreviewStyled.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ conversation: PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ lastMessageAt: PropTypes.string,
+ metadata: PropTypes.object,
+ participants: PropTypes.arrayOf(
+ PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ displayName: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ isOnline: PropTypes.bool,
+ metadata: PropTypes.object,
+ role: PropTypes.oneOf(['assistant', 'system', 'user']),
+ }),
+ ),
+ readState: PropTypes.oneOf(['read', 'unread']),
+ subtitle: PropTypes.string,
+ title: PropTypes.string,
+ unreadCount: PropTypes.number,
+ }).isRequired,
+ focused: PropTypes.bool,
+ selected: PropTypes.bool,
+ slotProps: PropTypes.object,
+ slots: PropTypes.object,
+ unread: PropTypes.bool,
+} as any;
+
+const ChatConversationListTimestampStyled = React.forwardRef<
+ HTMLDivElement,
+ ConversationListTimestampProps
+>(function ChatConversationListTimestampStyled(props, ref) {
+ return (
+
+ );
+});
+
+ChatConversationListTimestampStyled.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ conversation: PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ lastMessageAt: PropTypes.string,
+ metadata: PropTypes.object,
+ participants: PropTypes.arrayOf(
+ PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ displayName: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ isOnline: PropTypes.bool,
+ metadata: PropTypes.object,
+ role: PropTypes.oneOf(['assistant', 'system', 'user']),
+ }),
+ ),
+ readState: PropTypes.oneOf(['read', 'unread']),
+ subtitle: PropTypes.string,
+ title: PropTypes.string,
+ unreadCount: PropTypes.number,
+ }).isRequired,
+ focused: PropTypes.bool,
+ selected: PropTypes.bool,
+ slotProps: PropTypes.object,
+ slots: PropTypes.object,
+ unread: PropTypes.bool,
+} as any;
+
+const ChatConversationListUnreadBadgeStyled = React.forwardRef<
+ HTMLDivElement,
+ ConversationListUnreadBadgeProps
+>(function ChatConversationListUnreadBadgeStyled(props, ref) {
+ return (
+
+ );
+});
// Default inline SVG for the 3-dot "more" icon (MoreHoriz style).
+ChatConversationListUnreadBadgeStyled.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ conversation: PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ lastMessageAt: PropTypes.string,
+ metadata: PropTypes.object,
+ participants: PropTypes.arrayOf(
+ PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ displayName: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ isOnline: PropTypes.bool,
+ metadata: PropTypes.object,
+ role: PropTypes.oneOf(['assistant', 'system', 'user']),
+ }),
+ ),
+ readState: PropTypes.oneOf(['read', 'unread']),
+ subtitle: PropTypes.string,
+ title: PropTypes.string,
+ unreadCount: PropTypes.number,
+ }).isRequired,
+ focused: PropTypes.bool,
+ selected: PropTypes.bool,
+ slotProps: PropTypes.object,
+ slots: PropTypes.object,
+ unread: PropTypes.bool,
+} as any;
+
function DefaultMoreIcon() {
return (
@@ -389,19 +646,52 @@ function DefaultMoreIcon() {
);
}
-const ChatConversationListItemActionsStyled = React.forwardRef(
- function ChatConversationListItemActionsStyled(props: any, ref) {
- return (
-
- {props.children ?? }
-
- );
- },
-);
+const ChatConversationListItemActionsStyled = React.forwardRef<
+ HTMLDivElement,
+ ConversationListItemActionsProps
+>(function ChatConversationListItemActionsStyled(props, ref) {
+ return (
+
+ {props.children ?? }
+
+ );
+});
+
+ChatConversationListItemActionsStyled.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ conversation: PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ lastMessageAt: PropTypes.string,
+ metadata: PropTypes.object,
+ participants: PropTypes.arrayOf(
+ PropTypes.shape({
+ avatarUrl: PropTypes.string,
+ displayName: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ isOnline: PropTypes.bool,
+ metadata: PropTypes.object,
+ role: PropTypes.oneOf(['assistant', 'system', 'user']),
+ }),
+ ),
+ readState: PropTypes.oneOf(['read', 'unread']),
+ subtitle: PropTypes.string,
+ title: PropTypes.string,
+ unreadCount: PropTypes.number,
+ }).isRequired,
+ focused: PropTypes.bool,
+ selected: PropTypes.bool,
+ slotProps: PropTypes.object,
+ slots: PropTypes.object,
+ unread: PropTypes.bool,
+} as any;
const ChatConversationList = React.forwardRef(
function ChatConversationList(inProps, ref) {
@@ -418,78 +708,91 @@ const ChatConversationList = React.forwardRef = {
+ root: slots?.root ?? ChatConversationListStyled,
+ scroller: slots?.scroller ?? ChatConversationListScrollerStyled,
+ viewport: slots?.viewport ?? ChatConversationListViewportStyled,
+ scrollbar: slots?.scrollbar ?? NoopScrollbar,
+ scrollbarThumb: slots?.scrollbarThumb ?? NoopScrollbar,
+ item: slots?.item ?? ChatConversationListItemSlot,
+ itemAvatar: slots?.itemAvatar ?? ChatConversationListItemAvatarStyled,
+ itemContent: slots?.itemContent ?? ChatConversationListItemContentStyled,
+ title: slots?.title ?? ChatConversationListTitleStyled,
+ preview: slots?.preview ?? ChatConversationListPreviewStyled,
+ timestamp: slots?.timestamp ?? ChatConversationListTimestampStyled,
+ unreadBadge: slots?.unreadBadge ?? ChatConversationListUnreadBadgeStyled,
+ itemActions: slots?.itemActions ?? ChatConversationListItemActionsStyled,
+ ...slots,
+ };
+
+ // The headless `root` slot is typed as `SlotComponentProps<'div', ...>`,
+ // which intentionally does NOT include `sx`. We funnel `sx` to the styled
+ // root via a localized assertion — strictly typing the rest of the slot
+ // wiring still catches signature drift.
+ const rootSlotProps = {
+ className: clsx(classes.root, isCompact && classes.compact, className),
+ sx,
+ ...slotProps?.root,
+ } as unknown as ConversationListRootSlotProps['root'];
+
+ const resolvedSlotProps: ConversationListRootSlotProps = {
+ ...slotProps,
+ root: rootSlotProps,
+ scroller: {
+ className: classes.scroller,
+ ...slotProps?.scroller,
+ },
+ item: (ownerState: ConversationListItemOwnerState) => {
+ const externalItemProps =
+ typeof slotProps?.item === 'function' ? slotProps.item(ownerState) : slotProps?.item;
+
+ return {
+ className: clsx(
+ classes.item,
+ ownerState.selected && classes.itemSelected,
+ ownerState.unread && classes.itemUnread,
+ ownerState.focused && classes.itemFocused,
+ ),
+ ...externalItemProps,
+ };
+ },
+ itemAvatar: {
+ className: classes.itemAvatar,
+ ...slotProps?.itemAvatar,
+ },
+ itemContent: {
+ className: classes.itemContent,
+ ...slotProps?.itemContent,
+ },
+ title: {
+ className: classes.itemTitle,
+ ...slotProps?.title,
+ },
+ preview: {
+ className: classes.itemPreview,
+ ...slotProps?.preview,
+ },
+ timestamp: {
+ className: classes.itemTimestamp,
+ ...slotProps?.timestamp,
+ },
+ unreadBadge: {
+ className: classes.itemUnreadBadge,
+ ...slotProps?.unreadBadge,
+ },
+ itemActions: {
+ className: classes.itemActions,
+ ...slotProps?.itemActions,
+ },
+ };
+
return (
({
- className: clsx(
- classes.item,
- ownerState?.selected && classes.itemSelected,
- ownerState?.unread && classes.itemUnread,
- ownerState?.focused && classes.itemFocused,
- ),
- ...(typeof slotProps?.item === 'function'
- ? (slotProps.item as (s: any) => any)(ownerState)
- : slotProps?.item),
- })) as any,
- itemAvatar: {
- className: classes.itemAvatar,
- ...slotProps?.itemAvatar,
- } as any,
- itemContent: {
- className: classes.itemContent,
- ...slotProps?.itemContent,
- } as any,
- title: {
- className: classes.itemTitle,
- ...slotProps?.title,
- } as any,
- preview: {
- className: classes.itemPreview,
- ...slotProps?.preview,
- } as any,
- timestamp: {
- className: classes.itemTimestamp,
- ...slotProps?.timestamp,
- } as any,
- unreadBadge: {
- className: classes.itemUnreadBadge,
- ...slotProps?.unreadBadge,
- } as any,
- itemActions: {
- className: classes.itemActions,
- ...slotProps?.itemActions,
- } as any,
- }}
+ slots={resolvedSlots}
+ slotProps={resolvedSlotProps}
/>
);
},
diff --git a/packages/x-chat/src/ChatMessage/ChatDateDivider.tsx b/packages/x-chat/src/ChatMessage/ChatDateDivider.tsx
index 096e3caf0dba1..b9db13f7c8aa7 100644
--- a/packages/x-chat/src/ChatMessage/ChatDateDivider.tsx
+++ b/packages/x-chat/src/ChatMessage/ChatDateDivider.tsx
@@ -2,6 +2,7 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
+import { SxProps, Theme } from '@mui/system';
import { MessageListDateDivider, type MessageListDateDividerProps } from '@mui/x-chat-headless';
import { styled, createUseThemeProps } from '../internals/zero-styled';
import { useChatMessageUtilityClasses, type ChatMessageClasses } from './chatMessageClasses';
@@ -10,6 +11,7 @@ const useThemeProps = createUseThemeProps('MuiChatDateDivider');
export interface ChatDateDividerProps extends MessageListDateDividerProps {
className?: string;
+ sx?: SxProps;
classes?: Partial;
}
@@ -49,7 +51,7 @@ const ChatDateDividerLabelStyled = styled('div', {
const ChatDateDivider = React.forwardRef(
function ChatDateDivider(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiChatDateDivider' });
- const { slots, slotProps, className, classes: classesProp, ...other } = props;
+ const { slots, slotProps, className, classes: classesProp, sx, ...other } = props;
const classes = useChatMessageUtilityClasses(classesProp);
return (
@@ -66,6 +68,7 @@ const ChatDateDivider = React.forwardRef(
...slotProps,
divider: {
className: clsx(classes.dateDivider, className),
+ sx,
...(slotProps?.divider as object),
} as any,
}}
@@ -87,6 +90,11 @@ ChatDateDivider.propTypes = {
messageId: PropTypes.string.isRequired,
slotProps: PropTypes.object,
slots: PropTypes.object,
+ sx: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
+ PropTypes.func,
+ PropTypes.object,
+ ]),
} as any;
export { ChatDateDivider };
diff --git a/packages/x-chat/src/ChatMessage/ChatMessage.test.tsx b/packages/x-chat/src/ChatMessage/ChatMessage.test.tsx
index 493238530e3ff..088d8d0b4b3e2 100644
--- a/packages/x-chat/src/ChatMessage/ChatMessage.test.tsx
+++ b/packages/x-chat/src/ChatMessage/ChatMessage.test.tsx
@@ -112,7 +112,7 @@ describe('ChatMessage', () => {
expect(document.querySelector('.MuiChatMessage-root')).not.toBe(null);
});
- it('forwards custom className to the message root via slotProps.messageRoot', () => {
+ it('forwards custom className to the message root via slotProps.message', () => {
render(
{
expect(document.querySelector('.my-custom-class')).not.toBe(null);
});
+
+ it("treats role:'user' messages from non-current users as outer messages (Alice case)", () => {
+ render(
+
+ {null}
+ ,
+ );
+
+ const aliceRoot = document.querySelector(
+ '.MuiChatMessage-root[aria-label="Message from Alice Chen"]',
+ );
+ const meRoot = document.querySelector('.MuiChatMessage-root[aria-label="Message from Me"]');
+
+ expect(aliceRoot).not.toBe(null);
+ expect(meRoot).not.toBe(null);
+
+ // Alice's role is 'user' — the role hook is preserved for downstream styling.
+ expect(aliceRoot!.classList.contains('MuiChatMessage-roleUser')).toBe(true);
+ // …but Alice is NOT the current user, so the bubble must NOT be marked as own.
+ expect(aliceRoot!.getAttribute('data-is-own-message')).toBe(null);
+ // The current user's own user-role message IS marked as own.
+ expect(meRoot!.getAttribute('data-is-own-message')).toBe('true');
+ });
+
+ it('honors slots.error in the children composition path', () => {
+ function CustomError() {
+ return custom error
;
+ }
+
+ render(
+
+
+ child content
+
+ ,
+ );
+
+ // Custom children render, and the error surface resolves through slots.error
+ // rather than being hardcoded to the default ChatMessageError.
+ expect(document.querySelector('[data-testid="custom-child"]')).not.toBe(null);
+ expect(document.querySelector('[data-testid="custom-error"]')).not.toBe(null);
+ });
});
diff --git a/packages/x-chat/src/ChatMessage/ChatMessage.tsx b/packages/x-chat/src/ChatMessage/ChatMessage.tsx
index 59da1649089f9..029527e06bfdb 100644
--- a/packages/x-chat/src/ChatMessage/ChatMessage.tsx
+++ b/packages/x-chat/src/ChatMessage/ChatMessage.tsx
@@ -3,17 +3,76 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { SxProps, Theme } from '@mui/system';
-import { useMessage } from '@mui/x-chat-headless';
-import { MessageRoot, type MessageRootProps } from '@mui/x-chat-headless';
+import {
+ MessageRoot,
+ type MessageRootProps,
+ useChatVariant,
+ useMessage,
+} from '@mui/x-chat-headless';
import { styled, createUseThemeProps } from '../internals/zero-styled';
import { useChatMessageUtilityClasses, type ChatMessageClasses } from './chatMessageClasses';
+import { ChatMessageError, type ChatMessageErrorProps } from '../ChatMessageError/ChatMessageError';
+import { ChatMessageAvatar, type ChatMessageAvatarProps } from './ChatMessageAvatar';
+import { ChatMessageContent, type ChatMessageContentProps } from './ChatMessageContent';
+import { ChatMessageMeta, type ChatMessageMetaProps } from './ChatMessageMeta';
+import { ChatMessageInlineMeta } from './ChatMessageInlineMeta';
+import { ChatMessageActions, type ChatMessageActionsProps } from './ChatMessageActions';
const useThemeProps = createUseThemeProps('MuiChatMessage');
-export interface ChatMessageProps extends MessageRootProps {
+export interface ChatMessageSlots {
+ /** The styled root element. */
+ root: React.ElementType;
+ /**
+ * The avatar component. Pass `null` to hide it and collapse the avatar grid track.
+ * Function form receives the message context and may return `null` for per-message hiding,
+ * but the grid track is only dropped when the slot itself is `null`.
+ */
+ avatar: React.ElementType | null;
+ /** The bubble component that renders message content. */
+ content: React.ElementType;
+ /**
+ * The external meta component (compact variant). Pass `null` to hide it.
+ */
+ meta: React.ElementType | null;
+ /**
+ * The inline meta component (default variant; rendered inside the bubble). Pass `null` to hide it.
+ */
+ inlineMeta: React.ElementType | null;
+ /** The error component rendered under the bubble when status === 'error'. */
+ error: React.ElementType;
+ /**
+ * The actions component, rendered under the bubble.
+ * Receives `{ messageId }` as props.
+ * Pass `null` (or omit) to hide actions entirely.
+ */
+ actions: React.ElementType | null;
+ /**
+ * The author-name label. Rendered by the surrounding `ChatMessageGroup`
+ * (default variant: above the bubble; compact variant: inside the message
+ * grid). Forwarded through `slots.message.authorName` from `ChatBox`. Pass
+ * `null` to hide.
+ */
+ authorName: React.ElementType | null;
+}
+
+export interface ChatMessageSlotProps {
+ root?: any;
+ avatar?: Partial;
+ content?: Partial;
+ meta?: Partial;
+ inlineMeta?: Record;
+ error?: Partial;
+ actions?: Partial;
+ authorName?: Record;
+}
+
+export interface ChatMessageProps extends Omit {
className?: string;
sx?: SxProps;
classes?: Partial;
+ slots?: Partial;
+ slotProps?: ChatMessageSlotProps;
}
const ChatMessageStyled = styled('div', {
@@ -30,10 +89,11 @@ const ChatMessageStyled = styled('div', {
isGrouped?: boolean;
variant?: string;
density?: string;
+ isOwnMessage?: boolean;
};
}>(({ theme, ownerState }) => {
const isCompact = ownerState?.variant === 'compact';
- const isUser = ownerState?.role === 'user';
+ const isOwnMessage = ownerState?.isOwnMessage ?? false;
const densityPaddingBlock: Record = {
compact: theme.spacing(0.25),
standard: theme.spacing(1),
@@ -56,18 +116,23 @@ const ChatMessageStyled = styled('div', {
fontFamily: theme.typography.fontFamily,
...(isGrouped
? {
- // Grouped: no avatar/authorName rows — content + meta on the same row.
- // Meta (✓ 10:55) sits top-right, aligned to the start of the content.
gridTemplateColumns: 'var(--MuiChatMessage-avatarSize) 1fr auto',
- gridTemplateAreas: '". content meta"',
+ gridTemplateRows: 'auto auto',
+ gridTemplateAreas: '". content meta" ". error ."',
}
: {
- // First in group: avatar spans authorName + content rows.
- // Meta (✓ 10:55) sits top-right on the same row as the author name.
gridTemplateColumns: 'var(--MuiChatMessage-avatarSize) 1fr auto',
- gridTemplateRows: 'auto auto',
- gridTemplateAreas: '"avatar authorName meta" "avatar content ."',
+ gridTemplateRows: 'auto auto auto',
+ gridTemplateAreas: '"avatar authorName meta" "avatar content ." ". error ."',
}),
+ // Avatar-less layout: collapse the reserved avatar grid track so the bubble
+ // and meta lane reclaim the row. Applies to both grouped and first-in-group.
+ '&.MuiChatMessage-noAvatar': {
+ gridTemplateColumns: '1fr auto',
+ gridTemplateAreas: isGrouped
+ ? '"content meta" "error ."'
+ : '"authorName meta" "content ." "error ."',
+ },
};
}
@@ -79,8 +144,10 @@ const ChatMessageStyled = styled('div', {
return {
display: 'grid',
gridTemplateColumns: isGrouped ? 'var(--MuiChatMessage-avatarSize) 1fr' : 'auto 1fr',
- gridTemplateRows: 'auto auto',
- gridTemplateAreas: isGrouped ? '". content" ". actions"' : '"avatar content" ". actions"',
+ gridTemplateRows: 'auto auto auto',
+ gridTemplateAreas: isGrouped
+ ? '". content" ". error" ". actions"'
+ : '"avatar content" ". error" ". actions"',
columnGap: theme.spacing(0.5),
width: '100%',
boxSizing: 'border-box',
@@ -95,21 +162,42 @@ const ChatMessageStyled = styled('div', {
paddingBlockEnd: theme.spacing(0.25),
},
fontFamily: theme.typography.fontFamily,
- ...(isUser && {
+ ...(isOwnMessage && {
gridTemplateColumns: isGrouped ? '1fr var(--MuiChatMessage-avatarSize)' : '1fr auto',
- gridTemplateAreas: isGrouped ? '"content ." "actions ."' : '"content avatar" "actions ."',
+ gridTemplateAreas: isGrouped
+ ? '"content ." "error ." "actions ."'
+ : '"content avatar" "error ." "actions ."',
paddingInlineStart: `calc(${theme.spacing(2)} + var(--MuiChatMessage-avatarSize))`,
paddingInlineEnd: theme.spacing(2),
}),
+ // Avatar-less layout: collapse the reserved avatar column and drop the phantom
+ // padding so the bubble fills the full content lane on both sides.
+ '&.MuiChatMessage-noAvatar': {
+ gridTemplateColumns: '1fr',
+ gridTemplateAreas: '"content" "error" "actions"',
+ paddingInlineStart: theme.spacing(2),
+ paddingInlineEnd: theme.spacing(2),
+ },
};
});
const ChatMessage = React.forwardRef(
function ChatMessage(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiChatMessage' });
- const { slots, slotProps, className, classes: classesProp, sx, messageId, ...other } = props;
+ const {
+ slots,
+ slotProps,
+ className,
+ classes: classesProp,
+ sx,
+ messageId,
+ children,
+ ...other
+ } = props;
const classes = useChatMessageUtilityClasses(classesProp);
const message = useMessage(messageId);
+ const variant = useChatVariant();
+ const isCompact = variant === 'compact';
const stateClasses = clsx(
message?.role === 'user' && classes.roleUser,
@@ -118,6 +206,61 @@ const ChatMessage = React.forwardRef(
message?.status === 'error' && classes.error,
);
+ // Slot resolution: `null` means "hide and adjust layout"; `undefined` means "use default".
+ const AvatarSlot = slots?.avatar;
+ const hasAvatar = AvatarSlot !== null;
+
+ // The error surface is resolved through the `error` slot in both branches so custom
+ // message composition via `children` can still customize (or keep consistent) the
+ // error rendering, matching the slot-driven path below.
+ const ErrorComp = (slots?.error ?? ChatMessageError) as typeof ChatMessageError;
+
+ // Build the inner tree from slots when no `children` were passed (slot-driven).
+ // When `children` are provided, render them as-is for backward compatibility.
+ let innerTree: React.ReactNode;
+ if (children !== undefined) {
+ innerTree = (
+
+ {children}
+
+
+ );
+ } else {
+ const ContentComp = (slots?.content ?? ChatMessageContent) as typeof ChatMessageContent;
+ const MetaSlot = slots?.meta;
+ const InlineMetaSlot = slots?.inlineMeta;
+ const ActionsSlot = slots?.actions;
+ const AvatarComp = (AvatarSlot ?? ChatMessageAvatar) as typeof ChatMessageAvatar;
+
+ const isStreaming = message?.status === 'streaming';
+ const hasMeta =
+ Boolean(message?.createdAt) || Boolean(message?.editedAt) || Boolean(message?.status);
+ const InlineMetaComp = (InlineMetaSlot ??
+ ChatMessageInlineMeta) as typeof ChatMessageInlineMeta;
+ const inlineMeta =
+ !isCompact && !isStreaming && hasMeta && InlineMetaSlot !== null
+ ? React.createElement(InlineMetaComp, slotProps?.inlineMeta ?? {})
+ : undefined;
+
+ const MetaComp = (MetaSlot ?? ChatMessageMeta) as typeof ChatMessageMeta;
+ const externalMeta =
+ isCompact && MetaSlot !== null ? : null;
+
+ innerTree = (
+
+ {hasAvatar && }
+
+ {externalMeta}
+
+ {ActionsSlot && (
+
+
+
+ )}
+
+ );
+ }
+
return (
(
{...other}
slots={{
root: slots?.root ?? ChatMessageStyled,
- ...slots,
}}
slotProps={{
- ...slotProps,
root: {
- className: clsx(classes.root, stateClasses, className),
+ className: clsx(classes.root, stateClasses, !hasAvatar && classes.noAvatar, className),
sx,
...slotProps?.root,
} as any,
}}
- />
+ >
+ {innerTree}
+
);
},
);
diff --git a/packages/x-chat/src/ChatMessage/ChatMessageActions.tsx b/packages/x-chat/src/ChatMessage/ChatMessageActions.tsx
index 4c26a58504108..82980074ff430 100644
--- a/packages/x-chat/src/ChatMessage/ChatMessageActions.tsx
+++ b/packages/x-chat/src/ChatMessage/ChatMessageActions.tsx
@@ -19,7 +19,7 @@ const ChatMessageActionsStyled = styled('div', {
name: 'MuiChatMessage',
slot: 'Actions',
overridesResolver: (_, styles) => styles.actions,
-})<{ ownerState?: { role?: string } }>(({ theme, ownerState }) => ({
+})<{ ownerState?: { role?: string; isOwnMessage?: boolean } }>(({ theme, ownerState }) => ({
gridArea: 'actions',
display: 'inline-flex',
alignItems: 'center',
@@ -34,7 +34,7 @@ const ChatMessageActionsStyled = styled('div', {
'.MuiChatMessage-root:hover &, .MuiChatMessage-root:focus-within &': {
opacity: 1,
},
- ...(ownerState?.role === 'user' && {
+ ...(ownerState?.isOwnMessage && {
justifySelf: 'end',
}),
backgroundColor: (theme.vars || theme).palette.background.paper,
diff --git a/packages/x-chat/src/ChatMessage/ChatMessageContent.test.tsx b/packages/x-chat/src/ChatMessage/ChatMessageContent.test.tsx
index a6db4b572d311..51609a295b7be 100644
--- a/packages/x-chat/src/ChatMessage/ChatMessageContent.test.tsx
+++ b/packages/x-chat/src/ChatMessage/ChatMessageContent.test.tsx
@@ -1,8 +1,8 @@
import * as React from 'react';
-import { createRenderer, screen } from '@mui/internal-test-utils';
+import { act, createRenderer, screen } from '@mui/internal-test-utils';
import { describe, expect, it } from 'vitest';
import type { ChatAdapter, ChatMessage } from '@mui/x-chat-headless';
-import { ChatRoot, MessageRoot } from '@mui/x-chat-headless';
+import { ChatRoot, MessageRoot, useChatStore } from '@mui/x-chat-headless';
import { ChatMessageContent } from './ChatMessageContent';
const { render } = createRenderer();
@@ -160,7 +160,7 @@ describe('ChatMessageContent', () => {
expect(screen.getByText('Deny')).not.toBe(null);
});
- it('tool icon shows first letter of toolName', () => {
+ it('renders a default tool icon SVG in the header', () => {
renderWithMessage({
id: 'm1',
role: 'assistant',
@@ -176,8 +176,167 @@ describe('ChatMessageContent', () => {
},
],
});
- // The icon component renders the first letter uppercased
- expect(screen.getByText('S')).not.toBe(null);
+ // The default icon is now an inline SVG inside the styled icon span,
+ // sitting in the summary header alongside the tool title.
+ const summary = document.querySelector('summary');
+ expect(summary).not.toBe(null);
+ expect(summary!.querySelector('svg')).not.toBe(null);
+ });
+
+ it('renders Input and Output as independently collapsible sections', () => {
+ renderWithMessage({
+ id: 'm1',
+ role: 'assistant',
+ parts: [
+ {
+ type: 'tool',
+ toolInvocation: {
+ toolCallId: 'tc1',
+ toolName: 'searchTool',
+ state: 'output-available',
+ input: { query: 'test' },
+ output: { result: 'found' },
+ title: 'Search Tool',
+ },
+ },
+ ],
+ });
+ // Find all — root + 2 sections (input + output) = 3.
+ const allDetails = document.querySelectorAll('details');
+ expect(allDetails.length).toBe(3);
+ expect(screen.getByText('Tool called:')).not.toBe(null);
+ expect(screen.getByText('Tool result:')).not.toBe(null);
+ });
+
+ it('renders a status icon with state aria-label when output is available', () => {
+ renderWithMessage({
+ id: 'm1',
+ role: 'assistant',
+ parts: [
+ {
+ type: 'tool',
+ toolInvocation: {
+ toolCallId: 'tc1',
+ toolName: 'searchTool',
+ state: 'output-available',
+ input: {},
+ output: {},
+ },
+ },
+ ],
+ });
+ // State component is a [role="status"] element with aria-label = locale state label.
+ const status = document.querySelector('[role="status"]');
+ expect(status).not.toBe(null);
+ expect(status!.getAttribute('aria-label')).toBe('Completed');
+ expect(status!.querySelector('svg')).not.toBe(null);
+ });
+
+ it('auto-opens the input section when the state transitions to approval-requested', () => {
+ let store!: ReturnType;
+ function CaptureStore() {
+ store = useChatStore();
+ return null;
+ }
+
+ render(
+
+
+
+
+
+ ,
+ );
+
+ // The input section starts collapsed while the tool is `input-available`.
+ const inputDetailsBefore = screen.getByText('Tool called:').closest('details');
+ expect(inputDetailsBefore).not.toBe(null);
+ expect(inputDetailsBefore!.hasAttribute('open')).toBe(false);
+
+ // The same section component is reused when the state advances to
+ // `approval-requested`; it must now auto-open so the input being approved is visible.
+ act(() => {
+ store.updateMessage('m1', {
+ parts: [
+ {
+ type: 'tool',
+ toolInvocation: {
+ toolCallId: 'tc1',
+ toolName: 'dangerousTool',
+ state: 'approval-requested',
+ input: { action: 'delete' },
+ },
+ },
+ ],
+ });
+ });
+
+ const inputDetailsAfter = screen.getByText('Tool called:').closest('details');
+ expect(inputDetailsAfter!.hasAttribute('open')).toBe(true);
+ });
+
+ it('keeps the other Material tool slots when only one part slot is overridden', () => {
+ function CustomSummary({ children }: { children?: React.ReactNode }) {
+ return {children} ;
+ }
+
+ render(
+
+
+
+
+ ,
+ );
+
+ // The overridden slot is used…
+ expect(document.querySelector('[data-testid="custom-summary"]')).not.toBe(null);
+ // …and the other Material defaults survive: the collapsible root + the input/output
+ // section `` elements (3 total) would collapse to plain divs if the partial
+ // override had replaced the whole slot map.
+ expect(document.querySelectorAll('details').length).toBe(3);
});
});
diff --git a/packages/x-chat/src/ChatMessage/ChatMessageContent.tsx b/packages/x-chat/src/ChatMessage/ChatMessageContent.tsx
index 23b3c392c8fbd..92f1c39cffd56 100644
--- a/packages/x-chat/src/ChatMessage/ChatMessageContent.tsx
+++ b/packages/x-chat/src/ChatMessage/ChatMessageContent.tsx
@@ -5,9 +5,148 @@ import clsx from 'clsx';
import { alpha } from '@mui/material/styles';
import { MessageContent, type MessageContentProps } from '@mui/x-chat-headless';
import { styled, createUseThemeProps } from '../internals/zero-styled';
+import { useCopyToClipboard } from '../internals/useCopyToClipboard';
import { useChatMessageUtilityClasses, type ChatMessageClasses } from './chatMessageClasses';
import { renderMarkdown } from './renderMarkdown';
+// ---------------------------------------------------------------------------
+// Inline SVG icons — kept module-local to avoid an @mui/icons-material dep.
+// ---------------------------------------------------------------------------
+
+function ToolDefaultIcon(props: React.SVGAttributes) {
+ return (
+
+
+
+ );
+}
+
+function ReasoningDefaultIcon(props: React.SVGAttributes) {
+ // Simplified brain — two lobes with a central seam.
+ return (
+
+
+
+
+ );
+}
+
+function StatusCheckIcon(props: React.SVGAttributes) {
+ return (
+
+
+
+ );
+}
+
+function StatusCrossIcon(props: React.SVGAttributes) {
+ return (
+
+
+
+
+ );
+}
+
+function StatusWarningIcon(props: React.SVGAttributes) {
+ return (
+
+
+
+
+
+ );
+}
+
+function StatusSpinnerIcon(props: React.SVGAttributes) {
+ return (
+
+
+
+ );
+}
+
+function CopySvgIcon() {
+ return (
+
+
+
+ );
+}
+
+function CheckSvgIcon() {
+ return (
+
+
+
+ );
+}
+
const useThemeProps = createUseThemeProps('MuiChatMessageContent');
export interface ChatMessageContentProps extends MessageContentProps {
@@ -32,8 +171,12 @@ const ChatMessageBubbleStyled = styled('div', {
name: 'MuiChatMessage',
slot: 'Bubble',
overridesResolver: (_, styles) => styles.bubble,
-})<{ ownerState?: { role?: string; variant?: string } }>(({ theme, ownerState }) => {
- const isUser = ownerState?.role === 'user';
+})<{ ownerState?: { role?: string; variant?: string; isOwnMessage?: boolean } }>(({
+ theme,
+ ownerState,
+}) => {
+ const isUserRole = ownerState?.role === 'user';
+ const isOwn = ownerState?.isOwnMessage ?? false;
const isCompact = ownerState?.variant === 'compact';
// Base text styles shared between default and compact modes.
@@ -41,7 +184,9 @@ const ChatMessageBubbleStyled = styled('div', {
fontSize: theme.typography.body2.fontSize,
lineHeight: theme.typography.body2.lineHeight,
wordBreak: 'break-word' as const,
- whiteSpace: (isUser ? 'pre-wrap' : 'normal') as React.CSSProperties['whiteSpace'],
+ // Plain text (user role) preserves user-typed newlines; markdown-rendered
+ // assistant content collapses whitespace as usual.
+ whiteSpace: (isUserRole ? 'pre-wrap' : 'normal') as React.CSSProperties['whiteSpace'],
maxWidth: '100%',
boxSizing: 'border-box' as const,
'& p': {
@@ -100,22 +245,23 @@ const ChatMessageBubbleStyled = styled('div', {
position: 'relative', // Anchor for inline meta (timestamp + status)
padding: theme.spacing(1, 1.5),
borderRadius: theme.shape.borderRadius,
- // Shrink to fit content width; align to the side that matches the message role.
- alignSelf: isUser ? 'flex-end' : 'flex-start',
+ // Shrink to fit content width; own messages align to the trailing edge so
+ // assistant and other-user bubbles share the leading edge.
+ alignSelf: isOwn ? 'flex-end' : 'flex-start',
'& pre': {
margin: 0,
borderRadius: theme.shape.borderRadius,
overflow: 'auto',
padding: theme.spacing(1),
fontSize: theme.typography.caption.fontSize,
- background: isUser
+ background: isOwn
? alpha(theme.palette.common.white, 0.15)
: (theme.vars || theme).palette.action.hover,
},
'& code': {
fontFamily: 'monospace',
fontSize: '0.875em',
- background: isUser
+ background: isOwn
? alpha(theme.palette.common.white, 0.15)
: (theme.vars || theme).palette.action.hover,
padding: '0.1em 0.3em',
@@ -137,14 +283,14 @@ const ChatMessageBubbleStyled = styled('div', {
background: 'none',
padding: 0,
},
- backgroundColor: isUser
+ backgroundColor: isOwn
? (theme.vars || theme).palette.primary.main
: (theme.vars || theme).palette.grey[100],
- ...(!isUser &&
+ ...(!isOwn &&
theme.applyStyles('dark', {
backgroundColor: (theme.vars || theme).palette.grey[800],
})),
- color: isUser
+ color: isOwn
? (theme.vars || theme).palette.primary.contrastText
: (theme.vars || theme).palette.text.primary,
};
@@ -159,13 +305,20 @@ const ChatToolPartDetailsStyled = styled('details', {
slot: 'ToolRoot',
shouldForwardProp: (prop) => prop !== 'ownerState',
})<{ ownerState?: { state?: string } }>(({ theme }) => ({
- border: `1px solid ${(theme.vars || theme).palette.divider}`,
- borderRadius: theme.shape.borderRadius,
- overflow: 'hidden',
- margin: theme.spacing(0.75, 0),
+ margin: theme.spacing(0.5, 0),
fontSize: theme.typography.caption.fontSize,
fontFamily: theme.typography.fontFamily,
whiteSpace: 'normal',
+ // Indent everything after the header (sections, errors, actions) with a
+ // shared left-border accent — gives the expanded body a clean tree look.
+ '& > summary ~ *': {
+ marginLeft: 8,
+ paddingLeft: theme.spacing(1.25),
+ borderLeft: `1px solid ${(theme.vars || theme).palette.divider}`,
+ },
+ '& > summary ~ * + *': {
+ marginTop: theme.spacing(0.25),
+ },
}));
// Collapsible root: manages open/close state, auto-opens when approval is requested
@@ -203,19 +356,35 @@ const ChatToolPartHeader = styled('summary', {
name: 'MuiChatMessage',
slot: 'ToolHeader',
})(({ theme }) => ({
- display: 'flex',
+ display: 'inline-flex',
alignItems: 'center',
- gap: theme.spacing(1),
- padding: theme.spacing(0.75, 1.25),
- backgroundColor: (theme.vars || theme).palette.action.hover,
+ gap: theme.spacing(0.625),
+ padding: theme.spacing(0.25, 0),
cursor: 'pointer',
userSelect: 'none',
listStyleType: 'none',
+ borderRadius: theme.shape.borderRadius,
'&::-webkit-details-marker': { display: 'none' },
'&::marker': { display: 'none' },
- // Separator between header and body when expanded
- 'details[open] > &': {
- borderBottom: `1px solid ${(theme.vars || theme).palette.divider}`,
+ // Chevron renders at the end of the header (after icon, title, state).
+ '&::after': {
+ content: '""',
+ display: 'inline-block',
+ width: 5,
+ height: 5,
+ borderRight: `1.25px solid ${(theme.vars || theme).palette.text.secondary}`,
+ borderBottom: `1.25px solid ${(theme.vars || theme).palette.text.secondary}`,
+ transform: 'rotate(-45deg)',
+ transition: 'transform 150ms ease',
+ flexShrink: 0,
+ marginLeft: theme.spacing(0.25),
+ opacity: 0.7,
+ },
+ 'details[open] > &::after': {
+ transform: 'rotate(45deg)',
+ },
+ '&:hover::after': {
+ opacity: 1,
},
}));
@@ -227,39 +396,12 @@ const ChatToolPartIconStyled = styled('span', {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
- width: 18,
- height: 18,
- borderRadius: typeof theme.shape.borderRadius === 'number' ? theme.shape.borderRadius / 2 : 2,
- // Default (expanded): show the tool icon character at normal size
- fontSize: '0.6rem',
- fontFamily: 'monospace',
- fontWeight: theme.typography.fontWeightBold,
+ width: 16,
+ height: 16,
flexShrink: 0,
- backgroundColor: (theme.vars || theme).palette.action.selected,
color: (theme.vars || theme).palette.text.secondary,
- userSelect: 'none',
+ fontSize: '1rem',
lineHeight: 1,
- transition: 'opacity 100ms',
- // Collapsed + hovering summary: hide icon char, show '+' hint
- 'details:not([open]) summary:hover &': {
- fontSize: 0,
- '&::before': {
- content: '"+"',
- fontSize: '0.8rem',
- fontWeight: theme.typography.fontWeightRegular,
- lineHeight: 1,
- },
- },
- // Expanded + hovering summary: hide icon char, show '−' hint
- 'details[open] summary:hover &': {
- fontSize: 0,
- '&::before': {
- content: '"−"',
- fontSize: '0.9rem',
- fontWeight: theme.typography.fontWeightRegular,
- lineHeight: 1,
- },
- },
}));
interface ChatToolPartIconProps extends React.HTMLAttributes {
@@ -267,16 +409,14 @@ interface ChatToolPartIconProps extends React.HTMLAttributes {
}
/**
- * Default icon: shows the first letter of the tool name.
+ * Default icon: a generic tool/wrench glyph.
* Replace per tool via `toolSlots` / `toolSlotProps` on `partProps.tool`.
*/
const ChatToolPartIconComponent = React.forwardRef(
function ChatToolPartIcon({ ownerState, ...rest }, ref) {
- const toolName = ownerState?.toolName ?? '';
- const letter = toolName.length > 0 ? toolName[0].toUpperCase() : '⚙';
return (
- {letter}
+
);
},
@@ -296,15 +436,13 @@ const ChatToolPartTitle = styled('div', {
name: 'MuiChatMessage',
slot: 'ToolTitle',
})(({ theme }) => ({
- fontFamily: 'monospace',
- fontSize: theme.typography.caption.fontSize,
+ fontSize: theme.typography.body2.fontSize,
fontWeight: theme.typography.fontWeightMedium,
color: (theme.vars || theme).palette.text.primary,
lineHeight: 1.4,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
- flex: 1,
minWidth: 0,
}));
@@ -328,6 +466,31 @@ function resolveToolStatePalette(state: string | undefined): ToolStatePalette {
}
}
+function resolveToolStatusIcon(state: string | undefined): React.ReactNode {
+ switch (state) {
+ case 'output-available':
+ return ;
+ case 'output-error':
+ case 'output-denied':
+ return ;
+ case 'approval-requested':
+ return ;
+ case 'input-streaming':
+ case 'input-available':
+ case 'approval-responded':
+ return ;
+ default:
+ return null;
+ }
+}
+
+const spinnerKeyframes = {
+ '@keyframes mui-chat-tool-spin': {
+ from: { transform: 'rotate(0deg)' },
+ to: { transform: 'rotate(360deg)' },
+ },
+};
+
const ChatToolPartState = styled('span', {
name: 'MuiChatMessage',
slot: 'ToolState',
@@ -338,73 +501,299 @@ const ChatToolPartState = styled('span', {
const color = isDefault
? (theme.vars || theme).palette.text.secondary
: (theme.vars || theme).palette[palette].main;
- // Use theme.palette (not theme.vars) for alpha so we always get a real color string.
- const bg = isDefault
- ? (theme.vars || theme).palette.action.selected
- : alpha(theme.palette[palette].main, 0.12);
return {
+ ...spinnerKeyframes,
display: 'inline-flex',
alignItems: 'center',
- padding: theme.spacing(0, 0.75),
- height: 18,
- borderRadius: 9,
- fontSize: '0.6rem',
- fontWeight: theme.typography.fontWeightMedium,
- textTransform: 'uppercase',
- letterSpacing: '0.06em',
+ justifyContent: 'center',
+ width: 14,
+ height: 14,
flexShrink: 0,
- whiteSpace: 'nowrap',
- backgroundColor: bg,
color,
+ fontSize: '0.875rem',
+ lineHeight: 1,
+ '& .MuiChatMessage-ToolStateSpinner': {
+ animation: 'mui-chat-tool-spin 1s linear infinite',
+ },
};
});
-const ChatToolPartSection = styled('div', {
+interface ChatToolPartStateRenderProps extends React.HTMLAttributes {
+ ownerState?: { state?: string };
+ children?: React.ReactNode;
+}
+
+const ChatToolPartStateComponent = React.forwardRef(
+ function ChatToolPartStateRender({ ownerState, children: _label, ...rest }, ref) {
+ const icon = resolveToolStatusIcon(ownerState?.state);
+ if (icon === null) {
+ return null;
+ }
+ return (
+
+ {icon}
+
+ );
+ },
+);
+
+ChatToolPartStateComponent.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ children: PropTypes.node,
+ ownerState: PropTypes.shape({
+ state: PropTypes.string,
+ }),
+} as any;
+
+const ChatToolPartSectionDetails = styled('details', {
name: 'MuiChatMessage',
slot: 'ToolSection',
-})(({ theme }) => ({
- padding: theme.spacing(0.75, 1.25),
- '& + &': {
- borderTop: `1px solid ${(theme.vars || theme).palette.divider}`,
- },
- '& strong': {
- display: 'block',
- fontSize: '0.6rem',
+ shouldForwardProp: (prop) => prop !== 'ownerState',
+})<{ ownerState?: { section?: string } }>(({ theme }) => ({
+ margin: 0,
+ fontSize: theme.typography.caption.fontSize,
+}));
+
+const ChatToolPartSectionSummaryStyled = styled('summary', {
+ name: 'MuiChatMessage',
+ slot: 'ToolSectionSummary',
+ shouldForwardProp: (prop) => prop !== 'ownerState',
+})<{ ownerState?: { section?: string } }>(({ theme }) => ({
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: theme.spacing(0.5),
+ cursor: 'pointer',
+ userSelect: 'none',
+ listStyleType: 'none',
+ fontSize: theme.typography.body2.fontSize,
+ color: (theme.vars || theme).palette.text.primary,
+ borderRadius: theme.shape.borderRadius,
+ padding: theme.spacing(0.125, 0),
+ '&::-webkit-details-marker': { display: 'none' },
+ '&::marker': { display: 'none' },
+ '& .MuiChatMessage-ToolSectionLabel': {
fontWeight: theme.typography.fontWeightMedium,
- textTransform: 'uppercase',
- letterSpacing: '0.06em',
- color: (theme.vars || theme).palette.text.disabled,
- marginBottom: theme.spacing(0.5),
+ color: (theme.vars || theme).palette.text.primary,
+ flexShrink: 0,
+ },
+ '& .MuiChatMessage-ToolSectionPreview': {
+ color: (theme.vars || theme).palette.text.secondary,
+ fontWeight: theme.typography.fontWeightRegular,
+ fontFamily: 'monospace',
+ fontSize: theme.typography.caption.fontSize,
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ minWidth: 0,
+ },
+ // Compact chevron — uses CSS-border square so it scales with currentColor.
+ '&::before': {
+ content: '""',
+ display: 'inline-block',
+ width: 5,
+ height: 5,
+ borderRight: `1.25px solid ${(theme.vars || theme).palette.text.secondary}`,
+ borderBottom: `1.25px solid ${(theme.vars || theme).palette.text.secondary}`,
+ transform: 'rotate(-45deg)',
+ transition: 'transform 150ms ease',
+ flexShrink: 0,
+ opacity: 0.7,
+ },
+ 'details[open] > &::before': {
+ transform: 'rotate(45deg)',
+ },
+ '&:hover::before': {
+ opacity: 1,
+ },
+ // Hide preview when the section is open
+ 'details[open] > & .MuiChatMessage-ToolSectionPreview': {
+ display: 'none',
},
}));
-const ChatToolPartSectionContent = styled('pre', {
+interface ChatToolPartSectionRenderProps extends React.HTMLAttributes {
+ ownerState?: { section?: 'input' | 'output'; state?: string };
+}
+
+function ChatToolPartSection({ ownerState, ...rest }: ChatToolPartSectionRenderProps) {
+ const section = ownerState?.section;
+ const state = ownerState?.state;
+ const initialOpen = React.useMemo(() => {
+ if (section === 'input') {
+ return state === 'input-streaming' || state === 'approval-requested';
+ }
+ if (section === 'output') {
+ return state === 'output-available';
+ }
+ return false;
+ }, [section, state]);
+ const [open, setOpen] = React.useState(initialOpen);
+
+ // The section component is reused across streamed invocation-state updates (same
+ // element identity), so `useState(initialOpen)` alone never reacts to post-mount
+ // transitions (e.g. `input-available` → `approval-requested`, or `output-available`).
+ // Mirror the auto-open behavior of `ChatToolPartRoot`: force open when the section
+ // newly becomes one that should be open, while leaving a user's manual collapse
+ // untouched otherwise (the effect only re-runs when `initialOpen` flips).
+ React.useEffect(() => {
+ if (initialOpen) {
+ setOpen(true);
+ }
+ }, [initialOpen]);
+
+ return (
+ ) => {
+ setOpen((event.currentTarget as HTMLDetailsElement).open);
+ }}
+ {...rest}
+ />
+ );
+}
+
+interface ChatToolPartSectionSummaryProps extends React.HTMLAttributes {
+ ownerState?: {
+ section?: 'input' | 'output';
+ summaryLabel?: string;
+ previewValue?: string;
+ };
+ children?: React.ReactNode;
+}
+
+const ChatToolPartSectionSummary = React.forwardRef(
+ function ChatToolPartSectionSummary({ ownerState, children, ...rest }, ref) {
+ const label = ownerState?.summaryLabel ?? (typeof children === 'string' ? children : '');
+ const preview = ownerState?.previewValue ?? '';
+ return (
+ }
+ ownerState={ownerState}
+ {...(rest as any)}
+ >
+ {label}:
+ {preview ? {preview} : null}
+
+ );
+ },
+);
+
+ChatToolPartSectionSummary.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ children: PropTypes.node,
+ ownerState: PropTypes.shape({
+ previewValue: PropTypes.string,
+ section: PropTypes.oneOf(['input', 'output']),
+ summaryLabel: PropTypes.string,
+ }),
+} as any;
+
+const ChatToolPartSectionContentWrapper = styled('div', {
name: 'MuiChatMessage',
slot: 'ToolSectionContent',
})(({ theme }) => ({
+ position: 'relative',
+ marginTop: theme.spacing(0.375),
+ border: `1px solid ${(theme.vars || theme).palette.divider}`,
+ borderRadius: theme.shape.borderRadius,
+ backgroundColor: (theme.vars || theme).palette.background.paper,
+ overflow: 'hidden',
+}));
+
+const ChatToolPartSectionContentPre = styled('pre')(({ theme }) => ({
margin: 0,
- padding: theme.spacing(0.75, 1),
- borderRadius: typeof theme.shape.borderRadius === 'number' ? theme.shape.borderRadius / 2 : 2,
- backgroundColor: (theme.vars || theme).palette.action.hover,
+ padding: theme.spacing(1, 1.25),
+ paddingRight: theme.spacing(4.5), // reserve room for the copy button
fontSize: theme.typography.caption.fontSize,
fontFamily: 'monospace',
lineHeight: 1.5,
overflow: 'auto',
- maxHeight: 240,
+ maxHeight: 320,
color: (theme.vars || theme).palette.text.primary,
whiteSpace: 'pre',
wordBreak: 'normal',
}));
+const ChatToolPartSectionCopyButton = styled('button')(({ theme }) => ({
+ position: 'absolute',
+ top: theme.spacing(0.375),
+ right: theme.spacing(0.375),
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: 24,
+ height: 24,
+ padding: 0,
+ background: 'transparent',
+ border: 'none',
+ borderRadius: typeof theme.shape.borderRadius === 'number' ? theme.shape.borderRadius / 2 : 2,
+ color: (theme.vars || theme).palette.text.secondary,
+ cursor: 'pointer',
+ lineHeight: 0,
+ fontSize: '0.875rem',
+ opacity: 0.6,
+ transition: theme.transitions.create(['background-color', 'color', 'opacity']),
+ '&:hover': {
+ backgroundColor: (theme.vars || theme).palette.action.hover,
+ color: (theme.vars || theme).palette.text.primary,
+ opacity: 1,
+ },
+ '&:focus-visible': {
+ outline: `2px solid ${(theme.vars || theme).palette.primary.main}`,
+ outlineOffset: 2,
+ opacity: 1,
+ },
+}));
+
+interface ChatToolPartSectionContentRenderProps extends React.HTMLAttributes {
+ ownerState?: { section?: 'input' | 'output' };
+ children?: React.ReactNode;
+}
+
+function ChatToolPartSectionContent({
+ ownerState: _ownerState,
+ children,
+ ...rest
+}: ChatToolPartSectionContentRenderProps) {
+ const text = typeof children === 'string' ? children : React.Children.toArray(children).join('');
+ const { copyState, copy } = useCopyToClipboard();
+ return (
+
+ {children}
+ {text.length > 0 ? (
+ copy(text)}
+ title={copyState === 'copied' ? 'Copied!' : 'Copy'}
+ aria-label={copyState === 'copied' ? 'Copied' : 'Copy to clipboard'}
+ >
+ {copyState === 'copied' ? : }
+
+ ) : null}
+
+ );
+}
+
const ChatToolPartError = styled('div', {
name: 'MuiChatMessage',
slot: 'ToolError',
})(({ theme }) => ({
- padding: theme.spacing(0.75, 1.25),
color: (theme.vars || theme).palette.error.main,
fontSize: theme.typography.caption.fontSize,
- borderTop: `1px solid ${(theme.vars || theme).palette.divider}`,
}));
const ChatToolPartActions = styled('div', {
@@ -413,9 +802,6 @@ const ChatToolPartActions = styled('div', {
})(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1),
- padding: theme.spacing(0.75, 1.25),
- borderTop: `1px solid ${(theme.vars || theme).palette.divider}`,
- backgroundColor: (theme.vars || theme).palette.action.hover,
}));
const toolActionButtonBase = ({ theme }: { theme: any }) => ({
@@ -470,9 +856,10 @@ const toolPartSlots = {
root: ChatToolPartRoot,
header: ChatToolPartHeader,
title: ChatToolPartTitle,
- state: ChatToolPartState,
+ state: ChatToolPartStateComponent,
icon: ChatToolPartIconComponent,
section: ChatToolPartSection,
+ sectionSummary: ChatToolPartSectionSummary,
sectionContent: ChatToolPartSectionContent,
error: ChatToolPartError,
actions: ChatToolPartActions,
@@ -488,51 +875,92 @@ const ChatReasoningPartRoot = styled('details', {
name: 'MuiChatMessage',
slot: 'ReasoningRoot',
})(({ theme }) => ({
- borderLeft: `2px solid ${(theme.vars || theme).palette.divider}`,
- paddingLeft: theme.spacing(1),
margin: theme.spacing(0.5, 0),
- '&[open] summary::marker, &[open] summary::-webkit-details-marker': {
- // handled via pseudo-element below
- },
+ fontSize: theme.typography.caption.fontSize,
}));
-const ChatReasoningPartSummary = styled('summary', {
+const ChatReasoningPartSummaryStyled = styled('summary', {
name: 'MuiChatMessage',
slot: 'ReasoningSummary',
})(({ theme }) => ({
cursor: 'pointer',
- fontSize: theme.typography.caption.fontSize,
- color: (theme.vars || theme).palette.text.disabled,
+ fontSize: theme.typography.body2.fontSize,
+ color: (theme.vars || theme).palette.text.secondary,
userSelect: 'none',
listStyleType: 'none',
- '&::-webkit-details-marker': {
- display: 'none',
- },
- '&::marker': {
- display: 'none',
- },
- display: 'flex',
+ display: 'inline-flex',
alignItems: 'center',
- gap: theme.spacing(0.5),
- '&::before': {
- content: '"▶"',
- fontSize: '0.5rem',
+ gap: theme.spacing(0.625),
+ padding: theme.spacing(0.25, 0),
+ '&::-webkit-details-marker': { display: 'none' },
+ '&::marker': { display: 'none' },
+ '& .MuiChatMessage-ReasoningIcon': {
+ display: 'inline-flex',
+ alignItems: 'center',
+ fontSize: '1rem',
+ color: (theme.vars || theme).palette.text.secondary,
+ flexShrink: 0,
+ },
+ '&::after': {
+ content: '""',
display: 'inline-block',
- transition: 'transform 0.15s ease',
+ width: 5,
+ height: 5,
+ borderRight: `1.25px solid ${(theme.vars || theme).palette.text.secondary}`,
+ borderBottom: `1.25px solid ${(theme.vars || theme).palette.text.secondary}`,
+ transform: 'rotate(-45deg)',
+ transition: 'transform 150ms ease',
+ flexShrink: 0,
+ marginLeft: theme.spacing(0.25),
+ opacity: 0.7,
},
- 'details[open] > &::before': {
- transform: 'rotate(90deg)',
+ 'details[open] > &::after': {
+ transform: 'rotate(45deg)',
+ },
+ '&:hover::after': {
+ opacity: 1,
},
}));
+interface ChatReasoningSummaryProps extends React.HTMLAttributes {
+ ownerState?: { streaming?: boolean };
+ children?: React.ReactNode;
+}
+
+const ChatReasoningPartSummary = React.forwardRef(
+ function ChatReasoningPartSummaryRender({ ownerState, children, ...rest }, ref) {
+ return (
+ } {...(rest as any)}>
+
+
+
+ {children}
+
+ );
+ },
+);
+
+ChatReasoningPartSummary.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ children: PropTypes.node,
+ ownerState: PropTypes.shape({
+ streaming: PropTypes.bool,
+ }),
+} as any;
+
const ChatReasoningPartContent = styled('div', {
name: 'MuiChatMessage',
slot: 'ReasoningContent',
})(({ theme }) => ({
marginTop: theme.spacing(0.5),
- fontSize: theme.typography.caption.fontSize,
- color: (theme.vars || theme).palette.text.disabled,
- fontStyle: 'italic',
+ marginLeft: 8,
+ paddingLeft: theme.spacing(1.25),
+ borderLeft: `1px solid ${(theme.vars || theme).palette.divider}`,
+ fontSize: theme.typography.body2.fontSize,
+ color: (theme.vars || theme).palette.text.secondary,
whiteSpace: 'pre-wrap',
lineHeight: 1.6,
}));
@@ -734,29 +1162,33 @@ const ChatMessageContent = React.forwardRef
diff --git a/packages/x-chat/src/ChatMessage/ChatMessageGroup.test.tsx b/packages/x-chat/src/ChatMessage/ChatMessageGroup.test.tsx
index 68644ede21ea8..a22fac5173155 100644
--- a/packages/x-chat/src/ChatMessage/ChatMessageGroup.test.tsx
+++ b/packages/x-chat/src/ChatMessage/ChatMessageGroup.test.tsx
@@ -1,8 +1,9 @@
import * as React from 'react';
-import { createRenderer } from '@mui/internal-test-utils';
+import { createRenderer, screen } from '@mui/internal-test-utils';
import { describe, expect, it } from 'vitest';
-import type { ChatAdapter } from '@mui/x-chat-headless';
+import { ChatRoot, type ChatAdapter } from '@mui/x-chat-headless';
import { ChatBox } from '../ChatBox/ChatBox';
+import { ChatMessageGroup } from './ChatMessageGroup';
const { render } = createRenderer();
@@ -71,4 +72,93 @@ describe('ChatMessageGroup', () => {
// One group per author run
expect(groups.length).toBeGreaterThanOrEqual(2);
});
+
+ it('renders the author label via the default authorName slot', () => {
+ render(
+
+ {null}
+ ,
+ );
+
+ expect(document.body.textContent).toContain('Alice Author');
+ });
+
+ it('overrides the author label via slots.message.authorName', () => {
+ function CustomLabel(props: { children?: React.ReactNode }) {
+ return [{props.children}] ;
+ }
+
+ render(
+
+
+ ,
+ );
+
+ expect(document.querySelector('[data-testid="custom-author-name"]')?.textContent).toBe(
+ '[Alice]',
+ );
+ });
+
+ it('hides the author label when slots.message.authorName is null', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(document.body.textContent).not.toContain('Alice Hidden');
+ });
+
+ it('forwards slots.message.root to ChatMessage instead of replacing the row', () => {
+ render(
+
+
+ ,
+ );
+
+ // The message body still renders — the root slot only swaps the root element,
+ // it must not replace the whole ChatMessage row.
+ expect(screen.getByText('Body text')).not.toBe(null);
+ // …and the custom element is applied as the message root.
+ expect(document.querySelector('section.MuiChatMessage-root')).not.toBe(null);
+ });
});
diff --git a/packages/x-chat/src/ChatMessage/ChatMessageGroup.tsx b/packages/x-chat/src/ChatMessage/ChatMessageGroup.tsx
index 5b8548e59a6b3..d1e58cd57384b 100644
--- a/packages/x-chat/src/ChatMessage/ChatMessageGroup.tsx
+++ b/packages/x-chat/src/ChatMessage/ChatMessageGroup.tsx
@@ -3,26 +3,31 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { SxProps, Theme } from '@mui/system';
-import {
- MessageGroup,
- type MessageGroupProps,
- useChatVariant,
- useMessage,
-} from '@mui/x-chat-headless';
+import { MessageGroup, type MessageGroupProps } from '@mui/x-chat-headless';
import { styled, createUseThemeProps } from '../internals/zero-styled';
import { useChatMessageUtilityClasses, type ChatMessageClasses } from './chatMessageClasses';
-import { ChatMessage } from './ChatMessage';
-import { ChatMessageAvatar } from './ChatMessageAvatar';
-import { ChatMessageContent } from './ChatMessageContent';
-import { ChatMessageMeta } from './ChatMessageMeta';
-import { ChatMessageInlineMeta } from './ChatMessageInlineMeta';
+import { ChatMessage, type ChatMessageSlots, type ChatMessageSlotProps } from './ChatMessage';
const useThemeProps = createUseThemeProps('MuiChatMessageGroup');
-export interface ChatMessageGroupProps extends MessageGroupProps {
+export interface ChatMessageGroupSlots {
+ /** The styled group wrapper element. */
+ root?: React.ElementType;
+ /** Nested slot map forwarded to the inner ChatMessage. */
+ message?: Partial;
+}
+
+export interface ChatMessageGroupSlotProps {
+ root?: Record;
+ message?: ChatMessageSlotProps;
+}
+
+export interface ChatMessageGroupProps extends Omit {
className?: string;
sx?: SxProps;
classes?: Partial;
+ slots?: ChatMessageGroupSlots;
+ slotProps?: ChatMessageGroupSlotProps;
}
const ChatMessageGroupStyled = styled('div', {
@@ -54,21 +59,24 @@ const ChatMessageGroupStyled = styled('div', {
gap: 0,
width: '100%',
marginBlockStart,
+ // When the avatar slot is hidden, drop the reserved avatar size so any
+ // descendant relying on the variable (padding, author-name offset) collapses too.
+ '&.MuiChatMessage-noAvatar': {
+ '--MuiChatMessage-avatarSize': '0px',
+ },
};
});
const ChatMessageGroupAuthorNameStyled = styled('div', {
name: 'MuiChatMessage',
slot: 'GroupAuthorName',
-})<{ ownerState?: { authorRole?: string; variant?: string } }>(({ theme, ownerState }) => ({
+})<{ ownerState?: { isOwnMessage?: boolean; variant?: string } }>(({ theme, ownerState }) => ({
fontSize: theme.typography.caption.fontSize,
fontWeight: theme.typography.fontWeightMedium,
color: (theme.vars || theme).palette.text.secondary,
marginBottom: 0,
...(ownerState?.variant === 'compact'
? {
- // Compact: author name lives inside the message grid in the "authorName" area.
- // It shares a row with the avatar. Flex to push timestamp to the right.
gridArea: 'authorName',
display: 'flex',
alignItems: 'center',
@@ -78,8 +86,7 @@ const ChatMessageGroupAuthorNameStyled = styled('div', {
color: (theme.vars || theme).palette.primary.main,
}
: {
- // Default: offset by avatar width, user right-aligned
- ...(ownerState?.authorRole === 'user'
+ ...(ownerState?.isOwnMessage
? {
textAlign: 'right' as const,
paddingInlineEnd: `calc(var(--MuiChatMessage-avatarSize) + ${theme.spacing(2)} + ${theme.spacing(0.5)})`,
@@ -90,6 +97,10 @@ const ChatMessageGroupAuthorNameStyled = styled('div', {
}),
}));
+function HiddenAuthorName() {
+ return null;
+}
+
const ChatMessageGroupTimestampStyled = styled('span', {
name: 'MuiChatMessage',
slot: 'GroupTimestamp',
@@ -101,32 +112,6 @@ const ChatMessageGroupTimestampStyled = styled('span', {
flexShrink: 0,
}));
-/**
- * Default content rendered inside ChatMessageGroup when no children are provided.
- * Uses the variant-aware styled components (avatar, content bubble, meta).
- */
-function ChatMessageGroupDefaultContent({ messageId }: { messageId: string }) {
- const variant = useChatVariant();
- const isCompact = variant === 'compact';
- const message = useMessage(messageId);
- const isStreaming = message?.status === 'streaming';
- const hasMeta =
- Boolean(message?.createdAt) || Boolean(message?.editedAt) || Boolean(message?.status);
- // In the default variant, meta is rendered inline inside the bubble.
- // Skip it during streaming — there is no timestamp yet, and the streaming state
- // is already communicated via the MuiChatMessage-streaming CSS class.
- const afterContent =
- !isCompact && !isStreaming && hasMeta ? : undefined;
-
- return (
-
-
-
- {isCompact && }
-
- );
-}
-
const ChatMessageGroup = React.forwardRef(
function ChatMessageGroup(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiChatMessageGroup' });
@@ -142,15 +127,42 @@ const ChatMessageGroup = React.forwardRef
} = props;
const classes = useChatMessageUtilityClasses(classesProp);
- // When no children are provided, render the default styled message layout.
- // This ensures ChatMessageGroup works as a standalone component with proper
- // styling for both default and compact variants.
+ // The inner-message slot map (`slots.message`) flows through to ChatMessage.
+ // `slots.root` (the group-level slot, handled below) is the group's own styled
+ // wrapper element. The message-level `root` slot lives inside `slots.message` and
+ // is interpreted by ChatMessage itself — we must NOT hoist it into the row
+ // component, otherwise a raw element (e.g. `'section'`) would receive `messageId`/
+ // `slots`/`slotProps` it can't read and the message body would disappear.
+ const innerMessageSlots = slots?.message;
+ const innerMessageSlotProps = slotProps?.message;
+
+ // Track whether the avatar slot is explicitly nulled — used to mirror the
+ // `noAvatar` class on the group wrapper so any descendant CSS that reads
+ // `var(--MuiChatMessage-avatarSize)` can collapse the reserved space.
+ const hasAvatar = innerMessageSlots?.avatar !== null;
+
+ // `authorName` is rendered by the headless MessageGroup (group-level), but
+ // consumer-facing it lives under `slots.message.authorName` so all per-row
+ // overrides cluster in one namespace. Null hides the label entirely.
+ const authorNameOverride = innerMessageSlots?.authorName;
+ const AuthorNameSlot =
+ authorNameOverride === null
+ ? (HiddenAuthorName as React.ElementType)
+ : ((authorNameOverride ?? ChatMessageGroupAuthorNameStyled) as React.ElementType);
+
+ // Render priority:
+ // 1. Explicit `children` (legacy: caller provided their own composition)
+ // 2. Inner ChatMessage instance with the forwarded nested slot map. The nested
+ // map carries `root`, which ChatMessage applies to its own styled root (and
+ // `slotProps.root` is applied there too), so we forward instead of replacing.
const resolvedChildren =
children ??
(messageId ? (
-
-
-
+
) : null);
return (
@@ -159,18 +171,17 @@ const ChatMessageGroup = React.forwardRef
messageId={messageId}
{...other}
slots={{
- group: slots?.group ?? ChatMessageGroupStyled,
- authorName: slots?.authorName ?? ChatMessageGroupAuthorNameStyled,
- groupTimestamp: slots?.groupTimestamp ?? ChatMessageGroupTimestampStyled,
- ...slots,
+ group: (slots?.root ?? ChatMessageGroupStyled) as React.ElementType,
+ authorName: AuthorNameSlot,
+ groupTimestamp: ChatMessageGroupTimestampStyled,
}}
slotProps={{
- ...slotProps,
group: {
- className: clsx(classes.group, className),
+ className: clsx(classes.group, !hasAvatar && classes.noAvatar, className),
sx,
- ...(slotProps?.group as object),
+ ...(slotProps?.root ?? {}),
} as any,
+ authorName: innerMessageSlotProps?.authorName as any,
}}
>
{resolvedChildren}
diff --git a/packages/x-chat/src/ChatMessage/ChatMessageInlineMeta.tsx b/packages/x-chat/src/ChatMessage/ChatMessageInlineMeta.tsx
index d42a3cb0e84b7..24e5cd65af27a 100644
--- a/packages/x-chat/src/ChatMessage/ChatMessageInlineMeta.tsx
+++ b/packages/x-chat/src/ChatMessage/ChatMessageInlineMeta.tsx
@@ -31,8 +31,8 @@ const ChatMessageInlineMetaSpacer = styled('span', {
const ChatMessageInlineMetaContainer = styled('span', {
name: 'MuiChatMessage',
slot: 'InlineMeta',
-})<{ ownerState?: { role?: string } }>(({ theme, ownerState }) => {
- const isUser = ownerState?.role === 'user';
+})<{ ownerState?: { role?: string; isOwnMessage?: boolean } }>(({ theme, ownerState }) => {
+ const isOwn = ownerState?.isOwnMessage ?? false;
return {
position: 'absolute',
@@ -47,13 +47,13 @@ const ChatMessageInlineMetaContainer = styled('span', {
pointerEvents: 'none',
userSelect: 'none',
// Light mode: primary.main is dark blue → white meta is readable.
- // Dark mode (user): primary.main becomes a lighter blue → switch to dark meta for contrast.
- color: isUser ? 'rgba(255,255,255,0.65)' : (theme.vars || theme).palette.text.disabled,
- ...(isUser &&
+ // Dark mode (own): primary.main becomes a lighter blue → switch to dark meta for contrast.
+ color: isOwn ? 'rgba(255,255,255,0.65)' : (theme.vars || theme).palette.text.disabled,
+ ...(isOwn &&
theme.applyStyles('dark', {
color: 'rgba(0,0,0,0.55)',
})),
- ...(!isUser &&
+ ...(!isOwn &&
theme.applyStyles('dark', {
color: 'rgba(255,255,255,0.45)',
})),
diff --git a/packages/x-chat/src/ChatMessage/ChatMessageMeta.tsx b/packages/x-chat/src/ChatMessage/ChatMessageMeta.tsx
index d18a5f353492f..643d72a9c2b14 100644
--- a/packages/x-chat/src/ChatMessage/ChatMessageMeta.tsx
+++ b/packages/x-chat/src/ChatMessage/ChatMessageMeta.tsx
@@ -21,26 +21,28 @@ const ChatMessageMetaStyled = styled('div', {
name: 'MuiChatMessage',
slot: 'Meta',
overridesResolver: (_, styles) => styles.meta,
-})<{ ownerState?: { role?: string; variant?: string } }>(({ theme, ownerState }) => ({
- gridArea: 'meta',
- display: 'flex',
- alignItems: 'center',
- gap: theme.spacing(0.5),
- fontSize: theme.typography.caption.fontSize,
- color: (theme.vars || theme).palette.text.disabled,
- lineHeight: 1.4,
- minHeight: '1.2em',
- // Compact: always right-align status + timestamp regardless of role.
- // Align to the top of the grid row so it stays at the top when content wraps.
- // Default: only right-align for user messages.
- ...((ownerState?.variant === 'compact' || ownerState?.role === 'user') && {
- justifyContent: 'flex-end',
- }),
- ...(ownerState?.variant === 'compact' && {
- alignSelf: 'start',
- whiteSpace: 'nowrap',
+})<{ ownerState?: { role?: string; variant?: string; isOwnMessage?: boolean } }>(
+ ({ theme, ownerState }) => ({
+ gridArea: 'meta',
+ display: 'flex',
+ alignItems: 'center',
+ gap: theme.spacing(0.5),
+ fontSize: theme.typography.caption.fontSize,
+ color: (theme.vars || theme).palette.text.disabled,
+ lineHeight: 1.4,
+ minHeight: '1.2em',
+ // Compact: always right-align status + timestamp regardless of ownership.
+ // Align to the top of the grid row so it stays at the top when content wraps.
+ // Default: only right-align for own messages.
+ ...((ownerState?.variant === 'compact' || ownerState?.isOwnMessage) && {
+ justifyContent: 'flex-end',
+ }),
+ ...(ownerState?.variant === 'compact' && {
+ alignSelf: 'start',
+ whiteSpace: 'nowrap',
+ }),
}),
-}));
+);
const ChatMessageStatusStyled = styled('span', {
name: 'MuiChatMessage',
diff --git a/packages/x-chat/src/ChatMessage/chatMessageClasses.ts b/packages/x-chat/src/ChatMessage/chatMessageClasses.ts
index 060a6298f3922..9b31159064038 100644
--- a/packages/x-chat/src/ChatMessage/chatMessageClasses.ts
+++ b/packages/x-chat/src/ChatMessage/chatMessageClasses.ts
@@ -37,6 +37,8 @@ export interface ChatMessageClasses {
streaming: string;
/** Applied when the message has an error status */
error: string;
+ /** Applied when the avatar slot is hidden (`slots.avatar: null`) so the grid drops the avatar track. */
+ noAvatar: string;
}
export type ChatMessageClassKey = keyof ChatMessageClasses;
@@ -63,6 +65,7 @@ export const chatMessageClasses: ChatMessageClasses = generateUtilityClasses('Mu
'roleAssistant',
'streaming',
'error',
+ 'noAvatar',
]);
const slots: Record = {
@@ -83,6 +86,7 @@ const slots: Record = {
roleAssistant: ['roleAssistant'],
streaming: ['streaming'],
error: ['error'],
+ noAvatar: ['noAvatar'],
};
export const useChatMessageUtilityClasses = (
diff --git a/packages/x-chat/src/ChatMessage/index.ts b/packages/x-chat/src/ChatMessage/index.ts
index dc14dffeb975b..e92f94c693f73 100644
--- a/packages/x-chat/src/ChatMessage/index.ts
+++ b/packages/x-chat/src/ChatMessage/index.ts
@@ -14,6 +14,8 @@ export { ChatMessageGroup } from './ChatMessageGroup';
export type { ChatMessageGroupProps } from './ChatMessageGroup';
export { ChatDateDivider } from './ChatDateDivider';
export type { ChatDateDividerProps } from './ChatDateDivider';
+export { ChatMessageError } from '../ChatMessageError/ChatMessageError';
+export type { ChatMessageErrorProps } from '../ChatMessageError/ChatMessageError';
export {
chatMessageClasses,
getChatMessageUtilityClass,
diff --git a/packages/x-chat/src/ChatMessageError/ChatMessageError.test.tsx b/packages/x-chat/src/ChatMessageError/ChatMessageError.test.tsx
new file mode 100644
index 0000000000000..615febbbdd240
--- /dev/null
+++ b/packages/x-chat/src/ChatMessageError/ChatMessageError.test.tsx
@@ -0,0 +1,177 @@
+import * as React from 'react';
+import { act, createRenderer, fireEvent, screen, waitFor } from '@mui/internal-test-utils';
+import { describe, expect, it, vi } from 'vitest';
+import type { ChatAdapter, ChatMessage } from '@mui/x-chat-headless';
+import { ChatRoot, MessageRoot, useChatStore } from '@mui/x-chat-headless';
+import { ChatMessage as ChatMessageComponent } from '../ChatMessage/ChatMessage';
+import { ChatMessageError } from './ChatMessageError';
+
+const { render } = createRenderer();
+
+function createAdapter(overrides: Partial = {}): ChatAdapter {
+ return {
+ async sendMessage() {
+ return new ReadableStream({
+ start(controller) {
+ controller.close();
+ },
+ });
+ },
+ ...overrides,
+ };
+}
+
+const errorMessage: ChatMessage = {
+ id: 'm1',
+ role: 'user',
+ status: 'error',
+ parts: [{ type: 'text', text: 'Failed' }],
+};
+
+const okMessage: ChatMessage = {
+ id: 'm2',
+ role: 'user',
+ status: 'sent',
+ parts: [{ type: 'text', text: 'Fine' }],
+};
+
+let storeRef: ReturnType | null = null;
+function StoreCapture() {
+ storeRef = useChatStore();
+ return null;
+}
+
+describe('ChatMessageError', () => {
+ it('renders nothing when there is no error for the message', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.queryByRole('alert')).to.equal(null);
+ });
+
+ it('renders the inline error card with a retry button when the error matches the message', () => {
+ storeRef = null;
+ render(
+
+
+
+ ,
+ );
+
+ expect(storeRef).not.to.equal(null);
+
+ act(() => {
+ storeRef!.setMessageError('m1', {
+ code: 'SEND_ERROR',
+ message: 'Send failed',
+ source: 'send',
+ recoverable: true,
+ retryable: true,
+ details: { messageId: 'm1' },
+ });
+ });
+
+ const alert = screen.getByRole('alert');
+ expect(alert.textContent).to.contain('Send failed');
+ expect(screen.getByRole('button', { name: 'Retry' })).not.to.equal(null);
+ });
+
+ it('invokes the runtime retry action when the retry button is clicked', async () => {
+ const sendMessage = vi.fn(async () => {
+ return new ReadableStream({
+ start(controller) {
+ controller.close();
+ },
+ });
+ });
+ storeRef = null;
+ render(
+
+
+
+ ,
+ );
+
+ act(() => {
+ storeRef!.setMessageError('m1', {
+ code: 'SEND_ERROR',
+ message: 'Send failed',
+ source: 'send',
+ recoverable: true,
+ retryable: true,
+ details: { messageId: 'm1' },
+ });
+ });
+
+ const retryButton = screen.getByRole('button', { name: 'Retry' });
+ fireEvent.click(retryButton);
+
+ await waitFor(() => {
+ expect(sendMessage).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('disables the retry button while another stream is in flight', () => {
+ storeRef = null;
+ render(
+
+
+
+ ,
+ );
+
+ act(() => {
+ storeRef!.setMessageError('m1', {
+ code: 'SEND_ERROR',
+ message: 'Send failed',
+ source: 'send',
+ recoverable: true,
+ retryable: true,
+ details: { messageId: 'm1' },
+ });
+ storeRef!.setStreaming(true);
+ });
+
+ const retryButton = screen.getByRole('button', { name: 'Retry' });
+ expect(retryButton).to.have.property('disabled', true);
+ });
+
+ it('forwards consumer-supplied classes to the message and retryButton slots', () => {
+ storeRef = null;
+ render(
+
+
+
+
+
+ ,
+ );
+
+ act(() => {
+ storeRef!.setMessageError('m1', {
+ code: 'SEND_ERROR',
+ message: 'Send failed',
+ source: 'send',
+ recoverable: true,
+ retryable: true,
+ details: { messageId: 'm1' },
+ });
+ });
+
+ const alert = screen.getByRole('alert');
+ expect(alert.classList.contains('custom-root')).to.equal(true);
+ const messageSpan = alert.querySelector('.custom-message');
+ expect(messageSpan).not.to.equal(null);
+ const retryButton = screen.getByRole('button', { name: 'Retry' });
+ expect(retryButton.classList.contains('custom-retry')).to.equal(true);
+ });
+});
diff --git a/packages/x-chat/src/ChatMessageError/ChatMessageError.tsx b/packages/x-chat/src/ChatMessageError/ChatMessageError.tsx
new file mode 100644
index 0000000000000..c946be8875cbe
--- /dev/null
+++ b/packages/x-chat/src/ChatMessageError/ChatMessageError.tsx
@@ -0,0 +1,178 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import clsx from 'clsx';
+import { SxProps, Theme } from '@mui/system';
+import Button from '@mui/material/Button';
+import {
+ MessageError,
+ type MessageErrorProps,
+ type MessageErrorOwnerState,
+ useChatLocaleText,
+ useChatStatus,
+ useMessage,
+} from '@mui/x-chat-headless';
+import { styled, createUseThemeProps } from '../internals/zero-styled';
+import {
+ useChatMessageErrorUtilityClasses,
+ type ChatMessageErrorClasses,
+} from './chatMessageErrorClasses';
+
+const useThemeProps = createUseThemeProps('MuiChatMessageError');
+
+function getErrorCardBackground(theme: Theme & { vars?: any }) {
+ if (theme.vars) {
+ return `rgba(${theme.vars.palette.error.mainChannel} / 0.08)`;
+ }
+ if (theme.palette.mode === 'dark') {
+ return 'rgba(244, 67, 54, 0.16)';
+ }
+ return 'rgba(244, 67, 54, 0.08)';
+}
+
+export interface ChatMessageErrorProps extends MessageErrorProps {
+ className?: string;
+ sx?: SxProps;
+ classes?: Partial;
+}
+
+const ChatMessageErrorRoot = styled('div', {
+ name: 'MuiChatMessageError',
+ slot: 'Root',
+ overridesResolver: (_, styles) => styles.root,
+})<{ ownerState?: MessageErrorOwnerState }>(({ theme, ownerState }) => ({
+ // Inline error card anchored in the message's `error` grid area.
+ gridArea: 'error',
+ display: 'flex',
+ alignItems: 'center',
+ gap: theme.spacing(1),
+ marginBlock: theme.spacing(0.5),
+ paddingBlock: theme.spacing(0.75),
+ paddingInline: theme.spacing(1.25),
+ maxWidth: '100%',
+ boxSizing: 'border-box',
+ border: `1px solid ${(theme.vars || theme).palette.error.main}`,
+ borderRadius: theme.shape.borderRadius,
+ color: (theme.vars || theme).palette.error.main,
+ backgroundColor: getErrorCardBackground(theme),
+ fontSize: theme.typography.body2.fontSize,
+ lineHeight: theme.typography.body2.lineHeight,
+ ...(ownerState?.isOwnMessage && {
+ justifySelf: 'end',
+ }),
+}));
+
+const ChatMessageErrorMessage = styled('span', {
+ name: 'MuiChatMessageError',
+ slot: 'Message',
+ overridesResolver: (_, styles) => styles.message,
+})({
+ flex: 1,
+ minWidth: 0,
+ wordBreak: 'break-word',
+});
+
+const ChatMessageErrorRetryButton = styled(Button, {
+ name: 'MuiChatMessageError',
+ slot: 'RetryButton',
+ overridesResolver: (_, styles) => styles.retryButton,
+})(({ theme }) => ({
+ flexShrink: 0,
+ minWidth: 0,
+ color: (theme.vars || theme).palette.error.main,
+}));
+
+const ChatMessageErrorSlot = React.forwardRef(
+ function ChatMessageErrorSlot(props, ref) {
+ const { ownerState, className, children, messageClassName, retryButtonClassName, ...other } =
+ props as {
+ ownerState: MessageErrorOwnerState;
+ className?: string;
+ children?: React.ReactNode;
+ messageClassName?: string;
+ retryButtonClassName?: string;
+ } & React.HTMLAttributes;
+ const localeText = useChatLocaleText();
+ const message = useMessage(ownerState.messageId);
+ const { isStreaming } = useChatStatus();
+
+ // While any stream is in flight, or the message itself is busy, disable the
+ // retry button to prevent double submissions.
+ const disabled =
+ !ownerState.retryable ||
+ isStreaming ||
+ message?.status === 'sending' ||
+ message?.status === 'streaming';
+
+ return (
+
+
+ {children ?? ownerState.chatError?.message}
+
+ {ownerState.retryable ? (
+ {
+ void ownerState.retry();
+ }}
+ >
+ {localeText.retryButtonLabel}
+
+ ) : null}
+
+ );
+ },
+);
+
+const ChatMessageError = React.forwardRef(
+ function ChatMessageError(inProps, ref) {
+ const props = useThemeProps({ props: inProps, name: 'MuiChatMessageError' });
+ const { slots, slotProps, className, classes: classesProp, sx, ...other } = props;
+ const classes = useChatMessageErrorUtilityClasses(classesProp);
+
+ return (
+
+ );
+ },
+);
+
+ChatMessageError.propTypes = {
+ // ----------------------------- Warning --------------------------------
+ // | These PropTypes are generated from the TypeScript type definitions |
+ // | To update them edit the TypeScript types and run "pnpm proptypes" |
+ // ----------------------------------------------------------------------
+ children: PropTypes.node,
+ classes: PropTypes.object,
+ className: PropTypes.string,
+ slotProps: PropTypes.object,
+ slots: PropTypes.object,
+ sx: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
+ PropTypes.func,
+ PropTypes.object,
+ ]),
+} as any;
+
+export { ChatMessageError };
diff --git a/packages/x-chat/src/ChatMessageError/chatMessageErrorClasses.ts b/packages/x-chat/src/ChatMessageError/chatMessageErrorClasses.ts
new file mode 100644
index 0000000000000..9878fe8b35a15
--- /dev/null
+++ b/packages/x-chat/src/ChatMessageError/chatMessageErrorClasses.ts
@@ -0,0 +1,33 @@
+import generateUtilityClass from '@mui/utils/generateUtilityClass';
+import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
+import composeClasses from '@mui/utils/composeClasses';
+
+export interface ChatMessageErrorClasses {
+ /** Styles applied to the root element. */
+ root: string;
+ /** Styles applied to the message text element. */
+ message: string;
+ /** Styles applied to the retry button. */
+ retryButton: string;
+}
+
+export type ChatMessageErrorClassKey = keyof ChatMessageErrorClasses;
+
+export function getChatMessageErrorUtilityClass(slot: string): string {
+ return generateUtilityClass('MuiChatMessageError', slot);
+}
+
+export const chatMessageErrorClasses: ChatMessageErrorClasses = generateUtilityClasses(
+ 'MuiChatMessageError',
+ ['root', 'message', 'retryButton'],
+);
+
+const slots: Record = {
+ root: ['root'],
+ message: ['message'],
+ retryButton: ['retryButton'],
+};
+
+export const useChatMessageErrorUtilityClasses = (
+ classes: Partial | undefined,
+): ChatMessageErrorClasses => composeClasses(slots, getChatMessageErrorUtilityClass, classes);
diff --git a/packages/x-chat/src/ChatMessageError/index.ts b/packages/x-chat/src/ChatMessageError/index.ts
new file mode 100644
index 0000000000000..07df2661576c9
--- /dev/null
+++ b/packages/x-chat/src/ChatMessageError/index.ts
@@ -0,0 +1,8 @@
+export { ChatMessageError } from './ChatMessageError';
+export type { ChatMessageErrorProps } from './ChatMessageError';
+export {
+ chatMessageErrorClasses,
+ getChatMessageErrorUtilityClass,
+ useChatMessageErrorUtilityClasses,
+} from './chatMessageErrorClasses';
+export type { ChatMessageErrorClasses, ChatMessageErrorClassKey } from './chatMessageErrorClasses';
diff --git a/packages/x-chat/src/ChatMessageList/ChatMessageList.test.tsx b/packages/x-chat/src/ChatMessageList/ChatMessageList.test.tsx
index 2539865414766..25c3949fe5dad 100644
--- a/packages/x-chat/src/ChatMessageList/ChatMessageList.test.tsx
+++ b/packages/x-chat/src/ChatMessageList/ChatMessageList.test.tsx
@@ -2,7 +2,10 @@ import * as React from 'react';
import { createRenderer, screen } from '@mui/internal-test-utils';
import { describe, expect, it } from 'vitest';
import type { ChatAdapter } from '@mui/x-chat-headless';
+import { ChatRoot } from '@mui/x-chat-headless';
import { ChatBox } from '../ChatBox/ChatBox';
+import { ChatConversation } from '../ChatConversation/ChatConversation';
+import { ChatMessageList } from './ChatMessageList';
const { render } = createRenderer();
@@ -36,4 +39,102 @@ describe('ChatMessageList', () => {
);
expect(screen.getByText('Hello')).not.toBe(null);
});
+
+ it('renders the default row when used standalone without renderItem', () => {
+ render(
+
+
+
+
+ ,
+ );
+ expect(document.querySelector('.MuiChatMessage-group')).not.toBe(null);
+ expect(document.querySelector('.MuiChatMessage-root')).not.toBe(null);
+ expect(document.querySelector('.MuiChatMessage-avatar')).not.toBe(null);
+ expect(document.querySelector('.MuiChatMessage-content')).not.toBe(null);
+ expect(screen.getByText('Hi')).not.toBe(null);
+ });
+
+ it('uses a custom renderItem when provided', () => {
+ render(
+
+
+ {id}
} />
+
+ ,
+ );
+ expect(screen.getByTestId('custom-row').textContent).toBe('m1');
+ // Default row chrome should not render when a custom renderItem wins.
+ expect(document.querySelector('.MuiChatMessage-root')).toBe(null);
+ });
+
+ it('honors a row slot override in the default render path', () => {
+ function CustomAvatar() {
+ return ;
+ }
+ render(
+
+
+
+
+ ,
+ );
+ // Custom avatar replaces the default, but the rest of the row chrome stays.
+ expect(screen.getByTestId('custom-avatar')).not.toBe(null);
+ expect(document.querySelector('.MuiChatMessage-group')).not.toBe(null);
+ expect(document.querySelector('.MuiChatMessage-root')).not.toBe(null);
+ expect(document.querySelector('.MuiChatMessage-content')).not.toBe(null);
+ });
+
+ it('groups against the rendered items subset, not the full conversation', () => {
+ render(
+
+
+
+
+ ,
+ );
+
+ // Only m2/m3 are rendered. m2 is the first row of the rendered list, so it must be
+ // treated as the start of a group (author label shown) — it would be wrongly grouped
+ // against the off-list m1 if grouping ran on the full conversation.
+ expect(screen.getByText('Two')).not.toBe(null);
+ expect(screen.queryByText('One')).toBe(null);
+ expect(document.body.textContent).toContain('Alice Author');
+ });
});
diff --git a/packages/x-chat/src/ChatMessageList/ChatMessageList.tsx b/packages/x-chat/src/ChatMessageList/ChatMessageList.tsx
index cc313a3532337..445fe43073457 100644
--- a/packages/x-chat/src/ChatMessageList/ChatMessageList.tsx
+++ b/packages/x-chat/src/ChatMessageList/ChatMessageList.tsx
@@ -6,6 +6,8 @@ import { SxProps, Theme } from '@mui/system';
import {
MessageListRoot,
type MessageListRootProps,
+ type MessageListRootSlots,
+ type MessageListRootSlotProps,
type MessageListRootHandle,
useChatDensity,
} from '@mui/x-chat-headless';
@@ -14,10 +16,33 @@ import {
useChatMessageListUtilityClasses,
type ChatMessageListClasses,
} from './chatMessageListClasses';
+import {
+ DefaultMessageItem,
+ type ChatMessageRowSlots,
+ type ChatMessageRowSlotProps,
+} from './DefaultMessageItem';
const useThemeProps = createUseThemeProps('MuiChatMessageList');
-export interface ChatMessageListProps extends MessageListRootProps {
+export interface ChatMessageListSlots extends MessageListRootSlots, Partial {}
+
+export interface ChatMessageListSlotProps
+ extends MessageListRootSlotProps, ChatMessageRowSlotProps {}
+
+export interface ChatMessageListProps extends Omit<
+ MessageListRootProps,
+ 'renderItem' | 'slots' | 'slotProps'
+> {
+ /**
+ * Render a custom row for each message. When omitted, the default row used by
+ * `ChatBox` is rendered (group → message → avatar → content, with conditional
+ * inline meta, external meta in compact variant, and message actions slot).
+ * Provide a function to fully replace the row; use slot overrides to tweak
+ * individual sub-components without losing the default chrome.
+ */
+ renderItem?: MessageListRootProps['renderItem'];
+ slots?: Partial;
+ slotProps?: ChatMessageListSlotProps;
className?: string;
sx?: SxProps;
classes?: Partial;
@@ -67,38 +92,114 @@ const ChatMessageListContentStyled = styled('div', {
};
});
+// The row renderer wants the nested `messagesList` and `message` families.
+// Everything else on `slots` belongs to `MessageListRoot` (list-level).
+const ROW_SLOT_KEYS: ReadonlyArray = ['messagesList', 'message'];
+
const ChatMessageList = React.forwardRef(
function ChatMessageList(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiChatMessageList' });
- const { slots, slotProps, className, classes: classesProp, sx, ...other } = props;
+ const {
+ renderItem: renderItemProp,
+ slots,
+ slotProps,
+ className,
+ classes: classesProp,
+ sx,
+ ...other
+ } = props;
const classes = useChatMessageListUtilityClasses(classesProp);
const density = useChatDensity();
+ // Partition slots/slotProps: row-level keys go to the default renderer,
+ // list-level keys go to MessageListRoot. Row-level entries are extracted
+ // even when a custom renderItem is provided so they don't get forwarded to
+ // MessageListRoot — which would reject unknown slot keys at runtime.
+ const { rowSlots, listSlots } = React.useMemo(() => {
+ const row: Partial = {};
+ const list: Partial = {};
+ if (slots) {
+ for (const key of Object.keys(slots) as Array) {
+ if ((ROW_SLOT_KEYS as ReadonlyArray).includes(key)) {
+ (row as any)[key] = slots[key];
+ } else {
+ (list as any)[key] = slots[key];
+ }
+ }
+ }
+ return { rowSlots: row, listSlots: list };
+ }, [slots]);
+
+ const { rowSlotProps, listSlotProps } = React.useMemo(() => {
+ const row: ChatMessageRowSlotProps = {};
+ const list: MessageListRootSlotProps = {};
+ if (slotProps) {
+ for (const key of Object.keys(slotProps) as Array) {
+ if ((ROW_SLOT_KEYS as ReadonlyArray).includes(key)) {
+ (row as any)[key] = slotProps[key];
+ } else {
+ (list as any)[key] = slotProps[key];
+ }
+ }
+ }
+ return { rowSlotProps: row, listSlotProps: list };
+ }, [slotProps]);
+
+ // Keep the default renderer stable; read latest slot overrides and the resolved
+ // `items` from refs so updates don't churn the virtualized list. `items` is read
+ // without destructuring so it still flows to MessageListRoot via `...other`.
+ const rowSlotsRef = React.useRef(rowSlots);
+ const rowSlotPropsRef = React.useRef(rowSlotProps);
+ const itemsRef = React.useRef(undefined);
+ rowSlotsRef.current = rowSlots;
+ rowSlotPropsRef.current = rowSlotProps;
+ itemsRef.current = (other as { items?: string[] }).items;
+
+ // Forward `index` (rendered-list relative) and the rendered `items` so the group
+ // computes grouping against the rendered list — otherwise a custom `items` subset
+ // would regroup against the full conversation and drop avatars/author labels.
+ const defaultRenderItem = React.useCallback(
+ ({ id, index }: { id: string; index: number }) => (
+
+ ),
+ [],
+ );
+
+ const renderItem = renderItemProp ?? defaultRenderItem;
+
return (
@@ -136,7 +237,14 @@ ChatMessageList.propTypes = {
items: PropTypes.arrayOf(PropTypes.string),
onReachTop: PropTypes.func,
overlay: PropTypes.node,
- renderItem: PropTypes.func.isRequired,
+ /**
+ * Render a custom row for each message. When omitted, the default row used by
+ * `ChatBox` is rendered (group → message → avatar → content, with conditional
+ * inline meta, external meta in compact variant, and message actions slot).
+ * Provide a function to fully replace the row; use slot overrides to tweak
+ * individual sub-components without losing the default chrome.
+ */
+ renderItem: PropTypes.func,
slotProps: PropTypes.object,
slots: PropTypes.object,
sx: PropTypes.oneOfType([
diff --git a/packages/x-chat/src/ChatMessageList/DefaultMessageItem.tsx b/packages/x-chat/src/ChatMessageList/DefaultMessageItem.tsx
new file mode 100644
index 0000000000000..01f3eaafcacba
--- /dev/null
+++ b/packages/x-chat/src/ChatMessageList/DefaultMessageItem.tsx
@@ -0,0 +1,77 @@
+'use client';
+import * as React from 'react';
+import { ChatMessageGroup } from '../ChatMessage/ChatMessageGroup';
+import type {
+ ChatBoxMessageSlots,
+ ChatBoxMessageSlotProps,
+ ChatBoxMessagesListSlots,
+ ChatBoxMessagesListSlotProps,
+} from '../ChatBox/ChatBox.types';
+
+/**
+ * Per-row slot interface shared by `ChatBox` and `ChatMessageList`.
+ *
+ * Reuses ChatBox's nested message-list / message families so the same
+ * slot path works at both levels.
+ */
+export interface ChatMessageRowSlots {
+ messagesList?: ChatBoxMessagesListSlots;
+ message?: ChatBoxMessageSlots;
+}
+
+export interface ChatMessageRowSlotProps {
+ messagesList?: ChatBoxMessagesListSlotProps;
+ message?: ChatBoxMessageSlotProps;
+}
+
+export interface DefaultMessageItemProps {
+ id: string;
+ /**
+ * Index of this row within the rendered list. Forwarded to the group so grouping
+ * (previous/next neighbor lookup) is computed against the rendered list rather than
+ * the full conversation when a custom `items` subset is used.
+ */
+ index?: number;
+ /** The rendered list's message ids. Forwarded to the group alongside `index`. */
+ items?: string[];
+ slots?: ChatMessageRowSlots;
+ slotProps?: ChatMessageRowSlotProps;
+}
+
+/**
+ * Default message-row composition used by `ChatBox` and `ChatMessageList`
+ * when no custom `renderItem` is provided.
+ *
+ * `slots.messagesList.group` swaps the group component itself.
+ * `slots.message` forwards (as the nested map) into the chosen group so it
+ * can hand it down to the inner `ChatMessage`. Presentational `null` slots
+ * collapse layout as before.
+ *
+ * Memoized because this is the per-row component inside the virtualized list:
+ * when scroll-driven re-renders pass the same id/slots/slotProps references
+ * (the parent reads them from refs), the shallow compare short-circuits.
+ */
+export const DefaultMessageItem = React.memo(function DefaultMessageItem({
+ id,
+ index,
+ items,
+ slots,
+ slotProps,
+}: DefaultMessageItemProps) {
+ const GroupSlot = (slots?.messagesList?.group ?? ChatMessageGroup) as typeof ChatMessageGroup;
+
+ return (
+
+ );
+});
diff --git a/packages/x-chat/src/ChatMessageSkeleton/ChatMessageSkeleton.tsx b/packages/x-chat/src/ChatMessageSkeleton/ChatMessageSkeleton.tsx
index 046b569c2ce71..c2e1b2b74cf2d 100644
--- a/packages/x-chat/src/ChatMessageSkeleton/ChatMessageSkeleton.tsx
+++ b/packages/x-chat/src/ChatMessageSkeleton/ChatMessageSkeleton.tsx
@@ -2,7 +2,7 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
-import { keyframes } from '@mui/system';
+import { keyframes, SxProps, Theme } from '@mui/system';
import useSlotProps from '@mui/utils/useSlotProps';
import type { SlotComponentProps } from '@mui/utils/types';
import { styled, createUseThemeProps } from '../internals/zero-styled';
@@ -68,6 +68,7 @@ export interface ChatMessageSkeletonProps {
*/
lines?: number;
className?: string;
+ sx?: SxProps;
classes?: Partial;
slots?: ChatMessageSkeletonSlots;
slotProps?: ChatMessageSkeletonSlotProps;
@@ -82,7 +83,7 @@ const ChatMessageSkeleton = React.forwardRef(function ChatMessageSkeleton(
ref: React.Ref,
) {
const props = useThemeProps({ props: inProps, name: 'MuiChatMessageSkeleton' });
- const { lines = 3, className, classes: classesProp, slots, slotProps, ...other } = props;
+ const { lines = 3, className, classes: classesProp, slots, slotProps, sx, ...other } = props;
const classes = useChatMessageSkeletonUtilityClasses(classesProp);
const Root = slots?.root ?? ChatMessageSkeletonRootStyled;
@@ -96,6 +97,7 @@ const ChatMessageSkeleton = React.forwardRef(function ChatMessageSkeleton(
additionalProps: {
ref,
className: clsx(classes.root, className),
+ sx,
},
});
@@ -124,6 +126,11 @@ ChatMessageSkeleton.propTypes = {
lines: PropTypes.number,
slotProps: PropTypes.object,
slots: PropTypes.object,
+ sx: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
+ PropTypes.func,
+ PropTypes.object,
+ ]),
} as any;
export { ChatMessageSkeleton };
diff --git a/packages/x-chat/src/ChatMessageSources/ChatMessageSources.tsx b/packages/x-chat/src/ChatMessageSources/ChatMessageSources.tsx
index 1f72d03ee105c..b074fc1ad8cdb 100644
--- a/packages/x-chat/src/ChatMessageSources/ChatMessageSources.tsx
+++ b/packages/x-chat/src/ChatMessageSources/ChatMessageSources.tsx
@@ -2,6 +2,7 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
+import { SxProps, Theme } from '@mui/system';
import useSlotProps from '@mui/utils/useSlotProps';
import type { SlotComponentProps } from '@mui/utils/types';
import { styled, createUseThemeProps } from '../internals/zero-styled';
@@ -71,6 +72,7 @@ export interface ChatMessageSourcesProps {
label?: string;
children?: React.ReactNode;
className?: string;
+ sx?: SxProps;
classes?: Partial;
slots?: ChatMessageSourcesSlots;
slotProps?: ChatMessageSourcesSlotProps;
@@ -92,6 +94,7 @@ const ChatMessageSources = React.forwardRef(function ChatMessageSources(
classes: classesProp,
slots,
slotProps,
+ sx,
...other
} = props;
const classes = useChatMessageSourcesUtilityClasses(classesProp);
@@ -108,6 +111,7 @@ const ChatMessageSources = React.forwardRef(function ChatMessageSources(
additionalProps: {
ref,
className: clsx(classes.root, className),
+ sx,
},
});
@@ -152,6 +156,11 @@ ChatMessageSources.propTypes = {
label: PropTypes.string,
slotProps: PropTypes.object,
slots: PropTypes.object,
+ sx: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
+ PropTypes.func,
+ PropTypes.object,
+ ]),
} as any;
export { ChatMessageSources };
diff --git a/packages/x-chat/src/index.ts b/packages/x-chat/src/index.ts
index 48421e92668bd..4d2f3e269a691 100644
--- a/packages/x-chat/src/index.ts
+++ b/packages/x-chat/src/index.ts
@@ -116,6 +116,18 @@ export type { ChatDateDividerProps } from './ChatMessage/ChatDateDivider';
export { chatMessageClasses, getChatMessageUtilityClass } from './ChatMessage/chatMessageClasses';
export type { ChatMessageClasses, ChatMessageClassKey } from './ChatMessage/chatMessageClasses';
+// ─── ChatMessageError ─────────────────────────────────────────────────────────
+export { ChatMessageError } from './ChatMessageError/ChatMessageError';
+export type { ChatMessageErrorProps } from './ChatMessageError/ChatMessageError';
+export {
+ chatMessageErrorClasses,
+ getChatMessageErrorUtilityClass,
+} from './ChatMessageError/chatMessageErrorClasses';
+export type {
+ ChatMessageErrorClasses,
+ ChatMessageErrorClassKey,
+} from './ChatMessageError/chatMessageErrorClasses';
+
// ─── ChatMessageList ──────────────────────────────────────────────────────────
export { ChatMessageList } from './ChatMessageList/ChatMessageList';
export type { ChatMessageListProps } from './ChatMessageList/ChatMessageList';
diff --git a/packages/x-chat/src/internals/useCopyToClipboard.ts b/packages/x-chat/src/internals/useCopyToClipboard.ts
new file mode 100644
index 0000000000000..7b16701384e00
--- /dev/null
+++ b/packages/x-chat/src/internals/useCopyToClipboard.ts
@@ -0,0 +1,51 @@
+'use client';
+import * as React from 'react';
+
+export type CopyState = 'idle' | 'copied';
+
+export interface UseCopyToClipboardResult {
+ copyState: CopyState;
+ copy: (value: string) => void;
+}
+
+export function useCopyToClipboard(resetMs: number = 2000): UseCopyToClipboardResult {
+ const [copyState, setCopyState] = React.useState('idle');
+ const resetTimerRef = React.useRef | null>(null);
+
+ React.useEffect(() => {
+ return () => {
+ if (resetTimerRef.current !== null) {
+ clearTimeout(resetTimerRef.current);
+ }
+ };
+ }, []);
+
+ const copy = React.useCallback(
+ (value: string) => {
+ // Guard against environments without the async Clipboard API
+ // (older browsers, insecure contexts, some test runners).
+ if (
+ typeof navigator === 'undefined' ||
+ typeof navigator.clipboard?.writeText !== 'function'
+ ) {
+ return;
+ }
+
+ navigator.clipboard.writeText(value).then(
+ () => {
+ setCopyState('copied');
+ if (resetTimerRef.current !== null) {
+ clearTimeout(resetTimerRef.current);
+ }
+ resetTimerRef.current = setTimeout(() => setCopyState('idle'), resetMs);
+ },
+ () => {
+ // Clipboard write failed (e.g. permissions denied) — no-op
+ },
+ );
+ },
+ [resetMs],
+ );
+
+ return { copyState, copy };
+}
diff --git a/packages/x-chat/src/themeAugmentation/components.ts b/packages/x-chat/src/themeAugmentation/components.ts
index 765b9d7c09588..2f7b5b3e6e65d 100644
--- a/packages/x-chat/src/themeAugmentation/components.ts
+++ b/packages/x-chat/src/themeAugmentation/components.ts
@@ -21,6 +21,11 @@ export interface ChatComponents {
styleOverrides?: ComponentsOverrides['MuiChatMessage'];
variants?: ComponentsVariants['MuiChatMessage'];
};
+ MuiChatMessageError?: {
+ defaultProps?: ComponentsProps['MuiChatMessageError'];
+ styleOverrides?: ComponentsOverrides['MuiChatMessageError'];
+ variants?: ComponentsVariants['MuiChatMessageError'];
+ };
MuiChatMessageList?: {
defaultProps?: ComponentsProps['MuiChatMessageList'];
styleOverrides?: ComponentsOverrides['MuiChatMessageList'];
diff --git a/packages/x-chat/src/themeAugmentation/overrides.ts b/packages/x-chat/src/themeAugmentation/overrides.ts
index 7da80555f255f..9b3c7de73bca4 100644
--- a/packages/x-chat/src/themeAugmentation/overrides.ts
+++ b/packages/x-chat/src/themeAugmentation/overrides.ts
@@ -2,6 +2,7 @@ import { type ChatCodeBlockClassKey } from '../ChatCodeBlock/chatCodeBlockClasse
import { type ChatConfirmationClassKey } from '../ChatConfirmation/chatConfirmationClasses';
import { type ChatBoxClassKey } from '../ChatBox/chatBoxClasses';
import { type ChatMessageClassKey } from '../ChatMessage/chatMessageClasses';
+import { type ChatMessageErrorClassKey } from '../ChatMessageError/chatMessageErrorClasses';
import { type ChatMessageListClassKey } from '../ChatMessageList/chatMessageListClasses';
import { type ChatConversationClassKey } from '../ChatConversation/chatConversationClasses';
import { type ChatComposerClassKey } from '../ChatComposer/chatComposerClasses';
@@ -20,6 +21,7 @@ export interface ChatComponentNameToClassKey {
MuiChatConfirmation: ChatConfirmationClassKey;
MuiChatBox: ChatBoxClassKey;
MuiChatMessage: ChatMessageClassKey;
+ MuiChatMessageError: ChatMessageErrorClassKey;
MuiChatMessageList: ChatMessageListClassKey;
MuiChatConversation: ChatConversationClassKey;
MuiChatComposer: ChatComposerClassKey;
diff --git a/packages/x-chat/src/themeAugmentation/props.ts b/packages/x-chat/src/themeAugmentation/props.ts
index d1afbe8c56507..48dc8eaff3960 100644
--- a/packages/x-chat/src/themeAugmentation/props.ts
+++ b/packages/x-chat/src/themeAugmentation/props.ts
@@ -2,6 +2,7 @@ import { type ChatCodeBlockProps } from '../ChatCodeBlock/ChatCodeBlock';
import { type ChatConfirmationProps } from '../ChatConfirmation/ChatConfirmation';
import { type ChatBoxProps } from '../ChatBox/ChatBox.types';
import { type ChatMessageProps } from '../ChatMessage/ChatMessage';
+import { type ChatMessageErrorProps } from '../ChatMessageError/ChatMessageError';
import { type ChatMessageListProps } from '../ChatMessageList/ChatMessageList';
import { type ChatConversationProps } from '../ChatConversation/ChatConversation';
import { type ChatComposerProps } from '../ChatComposer/ChatComposer';
@@ -19,6 +20,7 @@ export interface ChatComponentsPropsList {
MuiChatConfirmation: ChatConfirmationProps;
MuiChatBox: ChatBoxProps;
MuiChatMessage: ChatMessageProps;
+ MuiChatMessageError: ChatMessageErrorProps;
MuiChatMessageList: ChatMessageListProps;
MuiChatConversation: ChatConversationProps;
MuiChatComposer: ChatComposerProps;
diff --git a/scripts/buildApiDocs/chatSettings/getComponentInfo.ts b/scripts/buildApiDocs/chatSettings/getComponentInfo.ts
index 56df0e43df33b..c6486e6e7be3a 100644
--- a/scripts/buildApiDocs/chatSettings/getComponentInfo.ts
+++ b/scripts/buildApiDocs/chatSettings/getComponentInfo.ts
@@ -92,8 +92,10 @@ export function getComponentImports(name: string, filename: string) {
}
}
- return [
- `import { ${name} } from '${subdirectoryImportPath}';`,
- `import { ${name} } from '${rootImportPath}';`,
- ];
+ return Array.from(
+ new Set([
+ `import { ${name} } from '${subdirectoryImportPath}';`,
+ `import { ${name} } from '${rootImportPath}';`,
+ ]),
+ );
}
diff --git a/scripts/buildApiDocs/chatSettings/index.ts b/scripts/buildApiDocs/chatSettings/index.ts
index c1b4d85672d3e..169d201e9ceae 100644
--- a/scripts/buildApiDocs/chatSettings/index.ts
+++ b/scripts/buildApiDocs/chatSettings/index.ts
@@ -96,6 +96,8 @@ export default chatApiPages;
'suggestions/SuggestionsRoot.tsx',
// Internal implementation detail of ChatBox, not exported from the package index.
'ChatBox/ChatBoxContent.tsx',
+ // Default row builder shared by ChatBox and ChatMessageList, not part of the public API.
+ 'ChatMessageList/DefaultMessageItem.tsx',
// Internal default icon components, not intended as public API.
'icons/DefaultAttachIcon.tsx',
'icons/DefaultCloseIcon.tsx',
diff --git a/scripts/x-chat.exports.json b/scripts/x-chat.exports.json
index 9769f53900827..3760c1326af47 100644
--- a/scripts/x-chat.exports.json
+++ b/scripts/x-chat.exports.json
@@ -76,6 +76,11 @@
{ "name": "ChatMessageClassKey", "kind": "TypeAlias" },
{ "name": "ChatMessageContent", "kind": "Variable" },
{ "name": "ChatMessageContentProps", "kind": "Interface" },
+ { "name": "ChatMessageError", "kind": "Variable" },
+ { "name": "chatMessageErrorClasses", "kind": "Variable" },
+ { "name": "ChatMessageErrorClasses", "kind": "Interface" },
+ { "name": "ChatMessageErrorClassKey", "kind": "TypeAlias" },
+ { "name": "ChatMessageErrorProps", "kind": "Interface" },
{ "name": "ChatMessageGroup", "kind": "Variable" },
{ "name": "ChatMessageGroupProps", "kind": "Interface" },
{ "name": "ChatMessageInlineMeta", "kind": "Function" },
@@ -142,6 +147,7 @@
{ "name": "getChatConfirmationUtilityClass", "kind": "Function" },
{ "name": "getChatConversationListUtilityClass", "kind": "Function" },
{ "name": "getChatConversationUtilityClass", "kind": "Function" },
+ { "name": "getChatMessageErrorUtilityClass", "kind": "Function" },
{ "name": "getChatMessageListUtilityClass", "kind": "Function" },
{ "name": "getChatMessageSkeletonUtilityClass", "kind": "Function" },
{ "name": "getChatMessageSourcesUtilityClass", "kind": "Function" },