Skip to content

Conversation

@PBK-B
Copy link
Contributor

@PBK-B PBK-B commented Nov 26, 2025

πŸ”— Linked issue

null

❓ Type of change

  • πŸ“– Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • πŸ‘Œ Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

πŸ“š Description

added message slot to support custom message item

Example

/*
 * @Author: Bin
 * @Date: 2025-11-25
 * @FilePath: /nuxt-ui/playgrounds/nuxt/app/pages/chat1.tsx
 */
import { defineComponent } from 'vue'
import { Chat } from '@ai-sdk/vue'
import type { UIMessage } from 'ai'
import { getTextFromMessage } from '@nuxt/ui/utils/ai'

import type { ChatMessagesSlots } from '@nuxt/ui/components/ChatMessages.vue'

import { UChatMessages, UContainer, UChatMessage, UButton, MDC } from '#components'

export default defineComponent({
  setup(_, __) {
    const chat = new Chat<UIMessage>({
      messages: [
        {
          id: 'system-welcome',
          role: 'system',
          parts: [{
            type: 'text',
            text: `**Hello!**
Welcome to rag-search-bot`
          }]
        },
        {
          id: 'user-welcome',
          role: 'user',
          parts: [{
            type: 'text',
            text: `Hello, how are you?`
          }]
        }
      ]
    })

    return () => {
      return (
        <div>
          <UContainer>
            <UChatMessages
              status={chat.status}
              messages={chat.messages}
              shouldAutoScroll={true}
              shouldScrollToBottom={true}
              assistant={{
                avatar: {
                  icon: 'i-lucide-bot'
                },
                variant: 'soft'
              }}
            >
              {
                {
                  message(props) {
                    const { message } = props
                    return (
                      <>
                        <UChatMessage {...props} ui={{ content: 'bg-[unset] p-0' }}>
                          {{
                            content(props: UIMessage) {
                              // console.log('content props', props)
                              return (
                                <>
                                  <div style={{ background: 'color-mix(in oklab, var(--ui-bg-elevated) 50%, transparent)', padding: '1.5rem', borderRadius: '0.5rem' }}>
                                    <MDC unwrap="p" value={getTextFromMessage(props)} />
                                  </div>
                                  {
                                    message === chat.lastMessage && message?.role === 'user'
                                      ? (
                                        <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginTop: '0.5rem' }}>
                                          <p style={{ display: 'flex', alignItems: 'center', paddingRight: '0.25rem', color: '#fb2c36' }}>{chat.error?.message || 'this message failed'}</p>
                                          <UButton variant="ghost" color="error" icon="i-lucide-rotate-ccw" />
                                        </div>
                                      )
                                      : undefined
                                  }
                                </>
                              )
                            }
                          }}
                        </UChatMessage>
                      </>
                    )
                  }
                } satisfies ChatMessagesSlots
              }
            </UChatMessages>
          </UContainer>
        </div>
      )
    }
  }
})

Preview

img

πŸ“ Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@PBK-B PBK-B requested a review from benjamincanac as a code owner November 26, 2025 07:54
@github-actions github-actions bot added the v4 #4488 label Nov 26, 2025
Copy link
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

The getProxySlots() function should exclude the 'message' slot since it's a container-level slot for custom message rendering, not a slot that should be forwarded to the inner message component.

View Details
πŸ“ Patch Details
diff --git a/src/runtime/components/ChatMessages.vue b/src/runtime/components/ChatMessages.vue
index 5af9a82a..f1995db7 100644
--- a/src/runtime/components/ChatMessages.vue
+++ b/src/runtime/components/ChatMessages.vue
@@ -99,7 +99,7 @@ const props = withDefaults(defineProps<ChatMessagesProps>(), {
 })
 const slots = defineSlots<ChatMessagesSlots>()
 
-const getProxySlots = () => omit(slots, ['default', 'indicator', 'viewport'])
+const getProxySlots = () => omit(slots, ['default', 'indicator', 'viewport', 'message'])
 
 const appConfig = useAppConfig() as ChatMessages['AppConfig']
 

Analysis

getProxySlots() incorrectly includes 'message' slot in ChatMessages.vue

What fails: The getProxySlots() function at line 102 in src/runtime/components/ChatMessages.vue omits ['default', 'indicator', 'viewport'] but should also omit 'message', causing the message slot to be incorrectly forwarded to inner message components.

How to reproduce:

  1. Create a ChatMessages component with a custom #message slot
  2. Use that slot to render a custom message component
  3. The custom component (or UChatMessage) receives a #message slot template that incorrectly points to the parent's message slot

What happens: When getProxySlots() includes 'message', the code iterates over it (line 307-309) and creates a named slot template #message that forwards the parent's message slot into the component. This is semantically incorrect.

Expected behavior: The message slot is a container-level slot that replaces the entire message rendering - it should not be forwarded as a child slot to inner components. Only the slots defined in ChatMessageSlots (leading, content, actions) should be forwarded. This follows the established pattern in similar container components like BlogPosts.vue and ChangelogVersions.vue.

Line 102 should be:

const getProxySlots = () => omit(slots, ['default', 'indicator', 'viewport', 'message'])

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 26, 2025

npm i https://pkg.pr.new/@nuxt/ui@5535

commit: 265adf8

Copy link
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

The CSS selector for the last message height styling no longer works because the DOM structure changed - message elements are now wrapped in divs, breaking the direct child selector [&>article].

View Details
πŸ“ Patch Details
diff --git a/src/theme/chat-messages.ts b/src/theme/chat-messages.ts
index ca60a357..b480ed57 100644
--- a/src/theme/chat-messages.ts
+++ b/src/theme/chat-messages.ts
@@ -1,6 +1,6 @@
 export default {
   slots: {
-    root: 'w-full flex flex-col gap-1 flex-1 px-2.5 [&>article]:last-of-type:min-h-(--last-message-height)',
+    root: 'w-full flex flex-col gap-1 flex-1 px-2.5 [&>div]:last-of-type:min-h-(--last-message-height)',
     indicator: 'h-6 flex items-center gap-1 py-3 *:size-2 *:rounded-full *:bg-elevated [&>*:nth-child(1)]:animate-[bounce_1s_infinite] [&>*:nth-child(2)]:animate-[bounce_1s_0.15s_infinite] [&>*:nth-child(3)]:animate-[bounce_1s_0.3s_infinite]',
     viewport: 'absolute inset-x-0 top-[86%] data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]',
     autoScroll: 'rounded-full absolute right-1/2 translate-x-1/2 bottom-0'

Analysis

CSS selector for last message height styling doesn't match DOM structure

What fails: The CSS selector [&>article]:last-of-type:min-h-(--last-message-height) in src/theme/chat-messages.ts targets direct <article> children, but messages are now wrapped in <div> elements, so the selector never matches.

How to reproduce:

  1. Render ChatMessages component with messages
  2. Inspect the DOM - you'll see structure: <div class="root"> β†’ <div (wrapper)> β†’ <article (message)>
  3. The CSS rule for [&>article]:last-of-type will not apply because articles are not direct children of root

Result: The min-height CSS variable --last-message-height is calculated in JavaScript (line 269, 305 in ChatMessages.vue) but never applied because the selector doesn't match any elements. The spacing between the last message and bottom of container doesn't work correctly.

Expected: The last message wrapper should receive the min-height styling to create proper spacing, as calculated by updateLastMessageHeight().

Fix: Change selector from [&>article]:last-of-type to [&>div]:last-of-type to target the wrapper divs that are actual direct children of root.

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
@PBK-B
Copy link
Contributor Author

PBK-B commented Dec 1, 2025

@benjamincanac If you have time, please help me take a look at this, thank you.

Copy link
Member

@PBK-B Why don't you use the default slot and iterate over messages yourself? πŸ€”

@PBK-B
Copy link
Contributor Author

PBK-B commented Dec 1, 2025

@PBK-B Why don't you use the default slot and iterate over messages yourself?

want to reuse scrolling logic for message list

Comment on lines +312 to +315
v-bind="{
...messageData,
...(messageData.role === 'user' ? userProps : assistantProps)
}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message slot doesn't pass the message property to the slot scope, even though the type definition requires it. Users won't be able to access the message data through the slot props.

View Details
πŸ“ Patch Details
diff --git a/src/runtime/components/ChatMessages.vue b/src/runtime/components/ChatMessages.vue
index 1e5d975c..0cdbab27 100644
--- a/src/runtime/components/ChatMessages.vue
+++ b/src/runtime/components/ChatMessages.vue
@@ -309,6 +309,7 @@ onMounted(() => {
           :ref="(el: HTMLElement) => registerMessageRef(messageData.id, el)"
           name="message"
           :compact="compact"
+          :message="messageData"
           v-bind="{
             ...messageData,
             ...(messageData.role === 'user' ? userProps : assistantProps)

Analysis

Missing message property in ChatMessages slot

What fails: The message slot in ChatMessages.vue (line 308-316) doesn't pass the message property to slot scope, even though the ChatSlotMessageProps type definition (line 62) specifies message?: UIMessage should be available to consumers.

How to reproduce:

 </div>
    </template>
  </UChatMessages>
</template>

Result: The message property is undefined in the slot scope because only messageData's properties are spread (id, role, parts, etc.) without creating a message self-reference. The slot receives properties from spreading the UIMessage object, but not the full message object wrapped in a message property.

Expected: The message property should be available in the slot scope as messageData (the full UIMessage object), matching the type definition and consistent with how inner slots forward the message on line 327: :message="messageData".

Fix: Add explicit :message="messageData" binding to the slot element, ensuring the full message object is accessible to slot consumers.

</UChatMessage>
<slot
v-if="$slots.message"
:ref="(el: HTMLElement) => registerMessageRef(messageData.id, el)"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:ref="(el: HTMLElement) => registerMessageRef(messageData.id, el)"
:ref="(el: HTMLElement | null) => registerMessageRef(messageData.id, el)"

The ref callback type annotation is too narrow and will cause TypeScript errors. Vue's ref callbacks must accept null for cleanup, but the type only specifies HTMLElement.

View Details

Analysis

Ref callback type annotation missing null parameter

What fails: Template ref callback on line 309 of ChatMessages.vue has type annotation (el: HTMLElement) which doesn't accept null, violating Vue's ref callback contract that requires handling unmounting with null parameter.

How to reproduce: Run TypeScript strict mode type checking:

pnpm exec tsc --strict src/runtime/components/ChatMessages.vue

Result: When the slot element unmounts or is conditionally removed (via v-if), Vue calls the ref callback with null, but the function signature only accepts HTMLElement, causing a type mismatch.

Expected behavior: Per Vue documentation, ref callbacks must accept null as the parameter: "When the element is unmounted, the argument will be null."

Fix applied: Updated line 309 to include null in the type annotation: :ref="(el: HTMLElement | null) => registerMessageRef(messageData.id, el)"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants