Skip to content

fix(ChatMessage/ChatMessages): handle generic message and message duplication#6391

Open
sandros94 wants to merge 5 commits intov4from
fix/chatmessages-fixes
Open

fix(ChatMessage/ChatMessages): handle generic message and message duplication#6391
sandros94 wants to merge 5 commits intov4from
fix/chatmessages-fixes

Conversation

@sandros94
Copy link
Copy Markdown
Member

@sandros94 sandros94 commented Apr 23, 2026

🔗 Linked issue

Replaces #5259

❓ 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

Bring UChatMessage/UChatMessages in line with the AI SDK's UIMessage shape, propagate generics end to end, and deprecate the redundant message slot prop on UChatMessages.

Type chain, and why message is now redundant

The typing flows in a straight line:

  1. ChatMessageProps<TMetadata, TDataParts, TTools> extends UIMessage<TMetadata, TDataParts, TTools> — every field of UIMessage (id, role, parts, metadata) is a prop on <UChatMessage>.
  2. ChatMessageSlots.content is narrowed to UIMessage<…> & { content?: string } and is bound in the template via v-bind="omit(props, <chat-ui keys>)" — so the slot exposes each UIMessage field directly, and any future field added to UIMessage upstream reaches the slot without a source change here.
  3. ChatMessagesSlots<T extends UIMessage[]> inherits ChatMessageSlots<M, D, U> per message (via T[number] extends UIMessage<infer M, infer D, infer U>), so the forwarded leading/files/content/actions slots receive exactly the same typed props the child produces.

Because (1) and (2) already put every UIMessage key on the slot scope as top-level props, the message object passed to ChatMessagesSlots is by construction pure duplication — two supported paths to the same data, with the object form being the one that won't pick up future UIMessage additions cleanly.

Why deprecate instead of remove

#content="{ message }" has been the pattern every first-party example taught since the v4 major shipped, so removing it outright would break a lot of existing code. The JSDoc @deprecated route mirrors the treatment AI SDK v5 itself gave to content → parts — which is already reflected on ChatMessageProps.content: fully functional at runtime, flagged in IDE hover, canonical examples migrated to the destructured form.

What's included

  • Types — generics on ChatMessage{Props,Slots} and ChatMessages{Props,Slots}; narrowed + forward-compatible content slot; message field on ChatMessagesSlots marked @deprecated with an @example pointing at the new destructure.
  • Tests — guards that metadata is actually delivered to the content slot, that no ChatMessage-specific layout prop leaks into it, and that message is still present on forwarded slots for backwards compatibility.
  • Examples — every #content="{ message }" migrated to #content="{ id, role, parts }" across the component showcase, component docs, v4 migration guide, blog post, playground, and the layout skill.

Why not just make message the single source of truth?

Taking the opposite direction from v4 (making message a single nested object on every slot and dropping flat per-field props) is technically viable but meaningfully heavier.
It requires reshaping all four ChatMessageSlots signatures (not just content, since leading/files/actions would need message added for consistency), commits every consumer to message.foo access with no path to flatten later, and still has to address the v4 gap where metadata was advertised by the slot type but never bound at runtime. Going through @deprecated instead keeps flat access consistent across every slot at both component levels, preserves the existing #content="{ message }" pattern as a documented migration bridge, and mirrors how upstream AI SDK itself retired content in favor of parts.

Use this playground link to understand the duplicate slot data (atm I cannot show you the type errors that it might rise)

📝 Checklist

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

sandros94 and others added 3 commits April 23, 2026 14:47
Also mark `message` slot prop as deprecated. This is duplicate code that already comes from `UChatMessage` slot's data (which is the source of the types).

Co-authored-by: Dany <alwe.dev@gmail.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

The PR changes the ChatMessages content slot to surface message fields directly (destructuring { id, role, parts }) across docs, examples, and tests. Core runtime components (ChatMessage.vue and ChatMessages.vue) were updated to add generic typing for message shapes, adjust slot typings, and forward a computed/normalized message payload to slots. Template loops and per-part key expressions were updated to use parts and id. Tests were extended to assert the new slot payload shape and that component-specific props are not leaked into slot props.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the primary changes: adding generic message type support and handling redundant message prop duplication across ChatMessage and ChatMessages components.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing type system changes, deprecation rationale, and migration strategy across the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chatmessages-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
docs/content/docs/1.getting-started/3.migration/1.v4.md (1)

474-541: ⚠️ Potential issue | 🟡 Minor

Add the release badge to the updated migration section.

These examples now document the unreleased flat #content slot payload, so the containing “AI SDK v5 migration” heading should be marked with the Soon badge.

📝 Proposed docs update
-### AI SDK v5 migration (optional)
+### AI SDK v5 migration (optional) :badge{label="Soon" class="align-text-top"}

As per coding guidelines, docs/**/*.md: Add :badge{label="Soon" class="align-text-top"} to docs headings when introducing new features or fixes, as the docs deploy on merge but features ship on next npm release.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/1.getting-started/3.migration/1.v4.md` around lines 474 -
541, The "AI SDK v5 migration" heading needs the release badge added; update the
markdown heading that introduces the "AI SDK v5 migration" section by appending
:badge{label="Soon" class="align-text-top"} to the heading line so the section
shows the Soon badge when documenting the unreleased flat `#content` slot payload.
docs/content/blog/how-to-build-an-ai-chat.md (1)

582-601: ⚠️ Potential issue | 🟡 Minor

Add Soon badges to the headings containing the updated slot examples.

These snippets now rely on the unreleased flat UChatMessages #content`` slot payload, so readers need the release-status badge.

📝 Proposed docs update
-## Creating the chat page
+## Creating the chat page :badge{label="Soon" class="align-text-top"}

-## Integrating history in the chat page
+## Integrating history in the chat page :badge{label="Soon" class="align-text-top"}

-### Integrating with the chat
+### Integrating with the chat :badge{label="Soon" class="align-text-top"}

As per coding guidelines, docs/**/*.md: Add :badge{label="Soon" class="align-text-top"} to docs headings when introducing new features or fixes, as the docs deploy on merge but features ship on next npm release.

Also applies to: 863-882, 1048-1067

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/blog/how-to-build-an-ai-chat.md` around lines 582 - 601, Add a
"Soon" release-status badge to the headings that introduce the unreleased flat
UChatMessages `#content` slot examples; locate the headings that precede the
updated slot snippets (the sections referencing UChatReasoning and the
UChatMessages `#content` payload) and append :badge{label="Soon"
class="align-text-top"} to those headings so the docs show the Soon badge for
readers; apply the same change to the other affected sections noted (the blocks
around lines shown in the review: the examples at the UChatReasoning/ChatComark
usage and the other two ranges mentioned).
docs/content/docs/2.components/chat.md (1)

304-458: ⚠️ Potential issue | 🟡 Minor

Mark this unreleased slot-contract example with the Soon badge.

These examples now show the new destructured #content slot scope. Since docs publish on merge before the npm release, mark the relevant heading so users know this API may not be available yet.

Proposed fix
-## Client Setup
+## Client Setup :badge{label="Soon" class="align-text-top"}

As per coding guidelines, docs/**/*.md: Add :badge{label="Soon" class="align-text-top"} to docs headings when introducing new features or fixes, as the docs deploy on merge but features ship on next npm release.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/2.components/chat.md` around lines 304 - 458, The "Client
Setup" heading introduces an unreleased API (destructured `#content` slot scope)
and must be marked with the Soon badge; update the "Client Setup" heading in
this doc to include :badge{label="Soon" class="align-text-top"} so readers see
the "Soon" indicator for the new destructured `#content` slot usage (refer to
the heading text "Client Setup" and the example using the `#content` slot and
Chat class from `@ai-sdk/vue` to locate the section).
src/runtime/components/ChatMessages.vue (1)

12-46: ⚠️ Potential issue | 🟡 Minor

Preserve message generics for list-level action callbacks.

messages is generic over UIMessage[] but user and assistant still use the non-generic ChatMessageProps, so action callbacks lose custom metadata/data/tool typings when ChatMessagesProps<CustomMessage[]> is used.

The codebase already uses this extraction pattern in SlotBase (line 63–66); apply the same approach here:

Proposed fix
 type ChatMessages = ComponentConfig<typeof theme, AppConfig, 'chatMessages'>
 
+type ChatMessagePropsFor<T extends UIMessage[]>
+  = T[number] extends UIMessage<infer M, infer D, infer U>
+    ? ChatMessageProps<M, D, U>
+    : ChatMessageProps
+
 export interface ChatMessagesProps<T extends UIMessage[] = UIMessage[]> {
   messages?: T
@@
-  user?: Pick<ChatMessageProps, 'icon' | 'avatar' | 'variant' | 'side' | 'actions' | 'ui'>
+  user?: Pick<ChatMessagePropsFor<T>, 'icon' | 'avatar' | 'variant' | 'side' | 'actions' | 'ui'>
@@
-  assistant?: Pick<ChatMessageProps, 'icon' | 'avatar' | 'variant' | 'side' | 'actions' | 'ui'>
+  assistant?: Pick<ChatMessagePropsFor<T>, 'icon' | 'avatar' | 'variant' | 'side' | 'actions' | 'ui'>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/ChatMessages.vue` around lines 12 - 46, The user and
assistant props lose per-message generics because they reference non-generic
ChatMessageProps; change their types to preserve the messages generic T by
picking from ChatMessageProps<T[number]> instead of ChatMessageProps so action
callbacks keep custom message typings. Specifically, in ChatMessagesProps<T
extends UIMessage[] = UIMessage[]>, update user and assistant to use
Pick<ChatMessageProps<T[number]>, 'icon' | 'avatar' | 'variant' | 'side' |
'actions' | 'ui'> (same keys) so the props inherit the message-level generic
type.
🧹 Nitpick comments (1)
test/components/ChatMessages.spec.ts (1)

79-99: Tighten the actions slot assertions.

This currently passes if only one message forwards actions/message. Assert both role-specific invocations and the exact forwarded actions to make the regression test meaningful.

🧪 Proposed test strengthening
   it('forwards `message` to the actions slot', async () => {
+    const userActions = [{ icon: 'i-lucide-copy', label: 'Copy user' }]
+    const assistantActions = [{ icon: 'i-lucide-copy', label: 'Copy assistant' }]
     const captured: Parameters<Exclude<ChatMessagesSlots['actions'], undefined>>[0][] = []
     await mountSuspended(ChatMessages, {
       props: {
         ...props,
-        user: { actions: [{ icon: 'i-lucide-copy', label: 'Copy' }] },
-        assistant: { actions: [{ icon: 'i-lucide-copy', label: 'Copy' }] }
+        user: { actions: userActions },
+        assistant: { actions: assistantActions }
       },
       slots: {
         actions: (slotProps) => {
           captured.push(slotProps)
           return 'x'
@@
       }
     })
 
-    expect(captured.length).toBeGreaterThan(0)
-    for (const p of captured) {
-      expect(p).toHaveProperty('message')
-      expect(p).toHaveProperty('actions')
-    }
+    expect(captured).toHaveLength(props.messages.length)
+    expect(captured[0]).toMatchObject({ message: props.messages[0], actions: userActions })
+    expect(captured[1]).toMatchObject({ message: props.messages[1], actions: assistantActions })
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/components/ChatMessages.spec.ts` around lines 79 - 99, The test
currently only checks that the actions slot was called and that slotProps have
message and actions; tighten it by asserting role-specific invocations and exact
forwarded actions: when mounting ChatMessages with props.user.actions and
props.assistant.actions, inspect the captured array (captured:
Parameters<Exclude<ChatMessagesSlots['actions'], undefined>>[0][]) and assert
there are calls for both message.role === 'user' and message.role ===
'assistant', and for each call assert slotProps.actions deep-equals the
corresponding props.user.actions or props.assistant.actions so the slot receives
the exact arrays forwarded by ChatMessages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/content/docs/2.components/chat-messages.md`:
- Around line 423-426: The "Slots" heading in the updated slot API docs must be
marked unreleased: open docs/content/docs/2.components/chat-messages.md, locate
the section that documents the new flat slot payload (where the template uses
the slot signature template `#content`="{ id, parts }" and the v-for over
parts/:key uses `${id}-${part.type}-${index}`) and add the Soon badge to the
"Slots" heading by appending :badge{label="Soon" class="align-text-top"} so the
heading displays the Soon marker until the next npm release.

In `@src/runtime/components/ChatMessage.vue`:
- Line 35: The action handler is currently being called with the full component
props (leaking component-only fields like icon/avatar/actions/class/ui) instead
of the public UIMessage shape; change the invocation in ChatMessage.vue so
actions[].onClick is called with messageProps (the sanitized
UIMessage<TMetadata,TDataParts,TTools> object) rather than the component props
object, ensuring the callback signature for actions?.onClick(e, messageProps)
matches the declared UIMessage type and doesn't expose internal props.

In `@test/components/ChatMessage.spec.ts`:
- Line 5: The import currently mixes a runtime import and a type import; change
to two declarations by importing the component default (ChatMessage) normally
and importing the type separately using an explicit type import (import type {
ChatMessageSlots }) from the same module so the runtime symbol ChatMessage and
the type symbol ChatMessageSlots are on separate import lines.

In `@test/components/ChatMessages.spec.ts`:
- Line 5: The combined import pulls both the runtime component and a type; split
them so the runtime default export ChatMessages is imported normally and the
type ChatMessagesSlots is imported using a standalone "import type" statement;
update the existing import line to only import ChatMessages (default) and add a
separate "import type { ChatMessagesSlots }" line to satisfy the rule of
separate type imports.

---

Outside diff comments:
In `@docs/content/blog/how-to-build-an-ai-chat.md`:
- Around line 582-601: Add a "Soon" release-status badge to the headings that
introduce the unreleased flat UChatMessages `#content` slot examples; locate the
headings that precede the updated slot snippets (the sections referencing
UChatReasoning and the UChatMessages `#content` payload) and append
:badge{label="Soon" class="align-text-top"} to those headings so the docs show
the Soon badge for readers; apply the same change to the other affected sections
noted (the blocks around lines shown in the review: the examples at the
UChatReasoning/ChatComark usage and the other two ranges mentioned).

In `@docs/content/docs/1.getting-started/3.migration/1.v4.md`:
- Around line 474-541: The "AI SDK v5 migration" heading needs the release badge
added; update the markdown heading that introduces the "AI SDK v5 migration"
section by appending :badge{label="Soon" class="align-text-top"} to the heading
line so the section shows the Soon badge when documenting the unreleased flat
`#content` slot payload.

In `@docs/content/docs/2.components/chat.md`:
- Around line 304-458: The "Client Setup" heading introduces an unreleased API
(destructured `#content` slot scope) and must be marked with the Soon badge;
update the "Client Setup" heading in this doc to include :badge{label="Soon"
class="align-text-top"} so readers see the "Soon" indicator for the new
destructured `#content` slot usage (refer to the heading text "Client Setup" and
the example using the `#content` slot and Chat class from `@ai-sdk/vue` to locate
the section).

In `@src/runtime/components/ChatMessages.vue`:
- Around line 12-46: The user and assistant props lose per-message generics
because they reference non-generic ChatMessageProps; change their types to
preserve the messages generic T by picking from ChatMessageProps<T[number]>
instead of ChatMessageProps so action callbacks keep custom message typings.
Specifically, in ChatMessagesProps<T extends UIMessage[] = UIMessage[]>, update
user and assistant to use Pick<ChatMessageProps<T[number]>, 'icon' | 'avatar' |
'variant' | 'side' | 'actions' | 'ui'> (same keys) so the props inherit the
message-level generic type.

---

Nitpick comments:
In `@test/components/ChatMessages.spec.ts`:
- Around line 79-99: The test currently only checks that the actions slot was
called and that slotProps have message and actions; tighten it by asserting
role-specific invocations and exact forwarded actions: when mounting
ChatMessages with props.user.actions and props.assistant.actions, inspect the
captured array (captured: Parameters<Exclude<ChatMessagesSlots['actions'],
undefined>>[0][]) and assert there are calls for both message.role === 'user'
and message.role === 'assistant', and for each call assert slotProps.actions
deep-equals the corresponding props.user.actions or props.assistant.actions so
the slot receives the exact arrays forwarded by ChatMessages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f997d2cb-36d0-4890-8d55-d586bb27b2eb

📥 Commits

Reviewing files that changed from the base of the PR and between b2a46d5 and bb3ca57.

📒 Files selected for processing (14)
  • docs/app/components/chat/Chat.vue
  • docs/app/components/content/examples/chat/ChatPaletteContentSearchExample.vue
  • docs/app/components/content/examples/chat/ChatPaletteModalExample.vue
  • docs/app/components/content/examples/sidebar/SidebarChatExample.vue
  • docs/content/blog/how-to-build-an-ai-chat.md
  • docs/content/docs/1.getting-started/3.migration/1.v4.md
  • docs/content/docs/2.components/chat-messages.md
  • docs/content/docs/2.components/chat.md
  • playgrounds/nuxt/app/pages/chat.vue
  • skills/nuxt-ui/references/layouts/chat.md
  • src/runtime/components/ChatMessage.vue
  • src/runtime/components/ChatMessages.vue
  • test/components/ChatMessage.spec.ts
  • test/components/ChatMessages.spec.ts

Comment thread docs/content/docs/2.components/chat-messages.md
Comment thread src/runtime/components/ChatMessage.vue
Comment thread test/components/ChatMessage.spec.ts
Comment thread test/components/ChatMessages.spec.ts
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 23, 2026

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

commit: 6f2f3a5

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/runtime/components/ChatMessage.vue (1)

112-116: Remove redundant 'text' in part guard.

part.type === 'text' narrows part to TextUIPart from the ai SDK, which guarantees text: string. The additional && 'text' in part is redundant at both type and runtime levels.

♻️ Suggested change
-            <template v-if="part.type === 'text' && 'text' in part">
+            <template v-if="part.type === 'text'">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/ChatMessage.vue` around lines 112 - 116, In the
ChatMessage.vue template, remove the redundant runtime/type guard "&& 'text' in
part" from the v-if because "part.type === 'text'" already narrows part to the
TextUIPart shape; update the template conditional on the element that renders
text (the v-if inside the v-for over parts) to only use part.type === 'text' so
you rely on the existing discriminated union and eliminate the unnecessary
check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/runtime/components/ChatMessage.vue`:
- Around line 112-116: In the ChatMessage.vue template, remove the redundant
runtime/type guard "&& 'text' in part" from the v-if because "part.type ===
'text'" already narrows part to the TextUIPart shape; update the template
conditional on the element that renders text (the v-if inside the v-for over
parts) to only use part.type === 'text' so you rely on the existing
discriminated union and eliminate the unnecessary check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 39185ed7-ee26-4062-8bfa-004248f1d84d

📥 Commits

Reviewing files that changed from the base of the PR and between bb3ca57 and 6f2f3a5.

📒 Files selected for processing (2)
  • src/runtime/components/ChatMessage.vue
  • src/runtime/components/ChatMessages.vue

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

Labels

bug Something isn't working typescript v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant