Skip to content

feat(chat): migrate to AI SDK v6 and AI Elements - #24

Merged
prudentbird merged 1 commit into
devfrom
feat/ai-sdk-v6-ai-elements-next-16
Apr 3, 2026
Merged

feat(chat): migrate to AI SDK v6 and AI Elements#24
prudentbird merged 1 commit into
devfrom
feat/ai-sdk-v6-ai-elements-next-16

Conversation

@prudentbird

@prudentbird prudentbird commented Apr 3, 2026

Copy link
Copy Markdown
Owner

Summary by cubic

Migrated the chat to AI SDK v6 and replaced the custom UI with AI Elements for a smoother, more reliable experience. Adds streaming markdown, a reasoning panel, and sticky scrolling.

  • New Features

    • Integrated AI Elements: Conversation, Message, PromptInput, Reasoning, and Shimmer.
    • Streaming markdown via streamdown with inline and block code styling.
    • Stick-to-bottom scrolling with a scroll-to-latest button.
  • Refactors

    • Replaced legacy chat input and message rendering; simplified layout and removed custom scroll logic.
    • Config/scripts: enabled logging.browserToTerminal, removed the experimental browser debug flag, dropped Turbopack from dev/build/preview, and switched code-quality to pnpm.
    • Dependencies: upgraded @ai-sdk/* packages, integrated streamdown, and added a Radix-based ui/collapsible wrapper.

Written for commit 481a3f3. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Redesigned conversation interface with enhanced message rendering and layout
    • Reasoning/thinking visualization with collapsible sections
    • Download conversations as markdown files
    • Improved chat input with streaming status indicators
    • Automatic scroll-to-bottom button for conversations
    • Animated text effects during streaming responses
  • Dependencies

    • Updated AI SDK, Next.js, and authentication libraries

@vercel

vercel Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fuseion Error Error Apr 3, 2026 10:23pm

@changeset-bot

changeset-bot Bot commented Apr 3, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 481a3f3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Apr 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR refactors the chat UI with new composable components for conversations, messages, and prompt input, updates multiple dependencies including Next.js and AI SDK packages, and moves browser debug configuration from experimental to logging settings in Next.js config.

Changes

Cohort / File(s) Summary
Configuration
next.config.ts, package.json
Relocated browser debug setting from experimental to logging config; upgraded Next.js, Next Auth, and AI SDK dependencies; added new UI and utility packages (@radix-ui/react-use-controllable-state, @streamdown/*, ai-elements, cmdk, motion, nanoid, use-stick-to-bottom); removed turbopack flags from build scripts.
AI UI Components
src/components/ai-elements/conversation.tsx, src/components/ai-elements/message.tsx, src/components/ai-elements/prompt-input.tsx, src/components/ai-elements/reasoning.tsx, src/components/ai-elements/shimmer.tsx, src/components/ai-elements/streamdown.tsx
Introduced composable React components for chat UI: conversation layout/download/scroll controls; message rendering with actions and toolbar; form-based prompt input with streaming status indicators; collapsible reasoning/thinking display with duration tracking; shimmer text animation effect; custom renderers for Streamdown markdown (code blocks, links).
UI Primitives
src/components/ui/collapsible.tsx
New wrapper around Radix UI Collapsible primitives with data-slot attributes for consistent slot identification.
Chat Component Refactoring
src/components/chat/index.tsx, src/components/chat/input.tsx, src/components/chat/messages/index.tsx, src/components/chat/messages/message.tsx
Simplified scroll container layout in main Chat component; replaced local textarea state and form handling in input with PromptInput wrapper; migrated message list from manual scroll/viewport tracking to Conversation layout wrapper; refactored individual message rendering to use composable Message, MessageContent, MessageResponse, and Reasoning components.

Sequence Diagram

sequenceDiagram
    actor User
    participant PromptInput as PromptInput Form
    participant Chat as Chat Component
    participant Messages as Messages Container
    participant Message as Message Component
    participant Reasoning as Reasoning Component
    
    User->>PromptInput: Type prompt & press Enter
    PromptInput->>PromptInput: Validate & trim text
    PromptInput->>Chat: onSubmit(text)
    Chat->>Chat: setMessages, update URL
    Chat->>Messages: Trigger re-render with new message
    Messages->>Message: Render user message
    Message->>Message: Display text via MessageResponse
    Messages->>Message: Render assistant message (streaming)
    Message->>Reasoning: Show thinking (if streaming)
    Reasoning->>Reasoning: Track streaming duration
    Message->>Message: Render response via MessageResponse
    Reasoning->>Reasoning: Auto-close after stream ends
    Messages->>Messages: Scroll to bottom via ConversationScrollButton
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰✨ A flutter of new components did play,
Composable pieces to light up the day!
From prompt to response, from reason to stream,
The chat UI now flows like a well-crafted dream.
With downloads and scrolls and a shimmer so bright,
This conversation takes UI to new height! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the primary changes: migration to AI SDK v6 and adoption of AI Elements components throughout the codebase.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ai-sdk-v6-ai-elements-next-16

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

4 issues found across 14 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/components/ai-elements/message.tsx">

<violation number="1" location="src/components/ai-elements/message.tsx:123">
P2: The custom `memo` comparator ignores `className` and other forwarded props, which can block required re-renders and leave stale output.</violation>
</file>

<file name="src/components/ai-elements/conversation.tsx">

<violation number="1" location="src/components/ai-elements/conversation.tsx:79">
P2: `ConversationScrollButton` allows `onClick` to be passed through and then spread after the internal handler, so external props can override the built-in `scrollToBottom` behavior.</violation>
</file>

<file name="src/components/ai-elements/shimmer.tsx">

<violation number="1" location="src/components/ai-elements/shimmer.tsx:15">
P2: The component advertises full `<span>` props but drops them by not forwarding remaining props to the rendered element.</violation>
</file>

<file name="src/components/ai-elements/streamdown.tsx">

<violation number="1" location="src/components/ai-elements/streamdown.tsx:25">
P1: Do not spread untrusted link props directly into `Link`; it allows unsafe `href` values and can override tab-safety attributes.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

"mx-0.5 rounded-md bg-muted px-[6px] py-0.5 text-sm text-muted-foreground",
className,
)}
{...props}

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Do not spread untrusted link props directly into Link; it allows unsafe href values and can override tab-safety attributes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/ai-elements/streamdown.tsx, line 25:

<comment>Do not spread untrusted link props directly into `Link`; it allows unsafe `href` values and can override tab-safety attributes.</comment>

<file context>
@@ -0,0 +1,71 @@
+          "mx-0.5 rounded-md bg-muted px-[6px] py-0.5 text-sm text-muted-foreground",
+          className,
+        )}
+        {...props}
+      />
+    );
</file context>
Fix with Cubic

components={streamdownComponents}
{...props}
/>
),

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The custom memo comparator ignores className and other forwarded props, which can block required re-renders and leave stale output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/ai-elements/message.tsx, line 123:

<comment>The custom `memo` comparator ignores `className` and other forwarded props, which can block required re-renders and leave stale output.</comment>

<file context>
@@ -0,0 +1,146 @@
+      components={streamdownComponents}
+      {...props}
+    />
+  ),
+  (prevProps, nextProps) =>
+    prevProps.children === nextProps.children &&
</file context>
Fix with Cubic

);
}

export type ConversationScrollButtonProps = ComponentProps<typeof Button>;

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: ConversationScrollButton allows onClick to be passed through and then spread after the internal handler, so external props can override the built-in scrollToBottom behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/ai-elements/conversation.tsx, line 79:

<comment>`ConversationScrollButton` allows `onClick` to be passed through and then spread after the internal handler, so external props can override the built-in `scrollToBottom` behavior.</comment>

<file context>
@@ -0,0 +1,180 @@
+  );
+}
+
+export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
+
+export function ConversationScrollButton({
</file context>
Fix with Cubic

duration?: number;
}) {
return (
<span

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The component advertises full <span> props but drops them by not forwarding remaining props to the rendered element.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/ai-elements/shimmer.tsx, line 15:

<comment>The component advertises full `<span>` props but drops them by not forwarding remaining props to the rendered element.</comment>

<file context>
@@ -0,0 +1,25 @@
+  duration?: number;
+}) {
+  return (
+    <span
+      className={cn(
+        "inline-block bg-[linear-gradient(110deg,currentColor,rgba(255,255,255,0.35),currentColor)] bg-[length:200%_100%] bg-clip-text text-transparent animate-[shimmer_1.8s_linear_infinite]",
</file context>
Fix with Cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/components/ui/collapsible.tsx (1)

4-29: Verify the Radix Collapsible import shape.

This wrapper aliases Collapsible from "radix-ui" and then treats it like a namespace with .Root, .CollapsibleTrigger, and .CollapsibleContent. That combination is easy to get wrong across Radix packages/versions, so please confirm the installed package actually exposes those members and align the import/member pattern before merge.

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

In `@src/components/ui/collapsible.tsx` around lines 4 - 29, The import currently
assumes a namespace-style export from "radix-ui" and then uses
CollapsiblePrimitive.Root, CollapsiblePrimitive.CollapsibleTrigger, and
CollapsiblePrimitive.CollapsibleContent; verify the installed Radix package
actually exports that shape and, if not, update the import to the correct
package and export names (for example the official package is often
"@radix-ui/react-collapsible" and exports named components like Root, Trigger,
Content) and adjust uses of CollapsiblePrimitive, CollapsiblePrimitive.Root,
CollapsiblePrimitive.CollapsibleTrigger, and
CollapsiblePrimitive.CollapsibleContent to match the actual exported
identifiers.
package.json (1)

63-63: Avoid latest for radix-ui.

Floating this dependency makes clean installs non-reproducible and can change the API that the new UI wrappers compile against on the next lockfile refresh or CI rebuild. Please pin it to an explicit version or semver range.

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

In `@package.json` at line 63, The dependency "radix-ui" is pinned to "latest",
making installs non-reproducible; update the package.json entry for "radix-ui"
to an explicit version or semver range (e.g., a specific version or a
caret/tilde range) instead of "latest", then run your package manager
(npm/yarn/pnpm) to regenerate the lockfile so CI and local builds are
deterministic; locate the "radix-ui" entry in package.json to make this change.
src/components/ai-elements/reasoning.tsx (1)

154-155: Minor copy polish: singular/plural duration text.

Line 154 renders 1 seconds. Consider pluralizing dynamically for cleaner UX.

Proposed fix
-  return <span>Thought for {duration} seconds</span>;
+  return <span>Thought for {duration} second{duration === 1 ? "" : "s"}</span>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ai-elements/reasoning.tsx` around lines 154 - 155, The
rendered text always uses "seconds" even when duration === 1; update the JSX in
the component that returns "<span>Thought for {duration} seconds</span>" to
pluralize correctly by checking the duration variable (e.g., use "{duration}
{duration === 1 ? 'second' : 'seconds'}") so it displays "1 second" for singular
and "N seconds" otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/ai-elements/conversation.tsx`:
- Around line 96-108: The icon-only Buttons in the Conversation component lack
accessible names; update the Button using handleScrollToBottom (and the similar
Button at the 166-178 block) to include stable screen-reader text by adding an
aria-label (e.g., aria-label="Scroll to bottom") and/or include an sr-only
<span> with descriptive text inside the Button to ensure screen readers get a
clear label while preserving the visual icon; make the change on the Button
instances in conversation.tsx that render ArrowDownIcon and the other icon-only
control.

In `@src/components/ai-elements/message.tsx`:
- Around line 75-99: MessageAction currently renders an empty <span
className="sr-only"> when neither label nor tooltip is provided, leaving the
Button without an accessible name; fix by computing a single accessibleName =
label ?? tooltip ?? "Action", pass that value as aria-label to the Button and
only render the sr-only span when accessibleName is truthy (so you never render
an empty sr-only node), updating references in the MessageAction function and
ensuring the Button receives aria-label={accessibleName}.
- Around line 113-127: The custom memo comparator on MessageResponse is
preventing updates when className or other Streamdown props change; remove the
second argument (the comparator function) from the memo(...) call so React uses
its default shallow comparison, ensuring changes to className, children,
isAnimating, or other props (passed into Streamdown via {...props}) will trigger
re-renders of MessageResponse (symbols: MessageResponse, Streamdown,
streamdownComponents, className, props).

In `@src/components/ai-elements/prompt-input.tsx`:
- Around line 107-117: The Enter key handler in handleKeyDown currently always
calls form.requestSubmit(), which bypasses the stop state; update handleKeyDown
to respect a generating/stop state (e.g., an isGenerating or disableEnterSubmit
prop passed from the parent) and when that state is true either prevent
submission and invoke the stop action or ignore the Enter key entirely instead
of calling requestSubmit(); ensure the parent (chat input) can pass the
generating flag so the stop button behavior in src/components/chat/input.tsx is
honored and Enter cannot trigger a new send while generating.

In `@src/components/ai-elements/reasoning.tsx`:
- Around line 65-71: The component's explicit-close detection and auto-close
lifecycle are broken: update the explicit-close check to consider both the
controlled prop and default (e.g., compute isExplicitlyClosed using
resolvedDefaultOpen === false || open === false so the controlled open={false}
prevents forced opens), ensure the auto-open effect that toggles isOpen during
streaming (the effect that watches streaming/streamId) respects
isExplicitlyClosed before forcing open, and add logic to reset hasAutoClosed
(setHasAutoClosed(false)) whenever a new stream cycle begins (watch streaming or
streamId) so the one-shot auto-close can run again on subsequent streams; these
changes involve isExplicitlyClosed, open/resolvedDefaultOpen,
useControllableState/isOpen, hasAutoClosed and the streaming-related effects.

In `@src/components/ai-elements/shimmer.tsx`:
- Around line 7-20: The Shimmer component claims React.ComponentProps<"span">
but currently drops all other span props; update Shimmer to collect the rest
props (e.g., function Shimmer({ children, className, duration = 1.8, ...props }:
React.ComponentProps<"span"> & { duration?: number }) and spread them onto the
<span> so aria-*, data-*, event handlers, etc. are forwarded. Keep merging
className via the existing cn(...) call and merge styles by combining
props.style with the computed animationDuration (e.g., { ...(props.style || {}),
animationDuration: `${duration}s` }) so external style keys are preserved while
ensuring the shimmer duration is applied.

---

Nitpick comments:
In `@package.json`:
- Line 63: The dependency "radix-ui" is pinned to "latest", making installs
non-reproducible; update the package.json entry for "radix-ui" to an explicit
version or semver range (e.g., a specific version or a caret/tilde range)
instead of "latest", then run your package manager (npm/yarn/pnpm) to regenerate
the lockfile so CI and local builds are deterministic; locate the "radix-ui"
entry in package.json to make this change.

In `@src/components/ai-elements/reasoning.tsx`:
- Around line 154-155: The rendered text always uses "seconds" even when
duration === 1; update the JSX in the component that returns "<span>Thought for
{duration} seconds</span>" to pluralize correctly by checking the duration
variable (e.g., use "{duration} {duration === 1 ? 'second' : 'seconds'}") so it
displays "1 second" for singular and "N seconds" otherwise.

In `@src/components/ui/collapsible.tsx`:
- Around line 4-29: The import currently assumes a namespace-style export from
"radix-ui" and then uses CollapsiblePrimitive.Root,
CollapsiblePrimitive.CollapsibleTrigger, and
CollapsiblePrimitive.CollapsibleContent; verify the installed Radix package
actually exports that shape and, if not, update the import to the correct
package and export names (for example the official package is often
"@radix-ui/react-collapsible" and exports named components like Root, Trigger,
Content) and adjust uses of CollapsiblePrimitive, CollapsiblePrimitive.Root,
CollapsiblePrimitive.CollapsibleTrigger, and
CollapsiblePrimitive.CollapsibleContent to match the actual exported
identifiers.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72de1b9d-5c5a-40b6-b4e7-f9182772e614

📥 Commits

Reviewing files that changed from the base of the PR and between 37c9045 and 481a3f3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • next.config.ts
  • package.json
  • src/components/ai-elements/conversation.tsx
  • src/components/ai-elements/message.tsx
  • src/components/ai-elements/prompt-input.tsx
  • src/components/ai-elements/reasoning.tsx
  • src/components/ai-elements/shimmer.tsx
  • src/components/ai-elements/streamdown.tsx
  • src/components/chat/index.tsx
  • src/components/chat/input.tsx
  • src/components/chat/messages/index.tsx
  • src/components/chat/messages/message.tsx
  • src/components/ui/collapsible.tsx

Comment on lines +96 to +108
<Button
className={cn(
"absolute bottom-4 left-1/2 z-10 size-10 -translate-x-1/2 rounded-full border bg-background/95 shadow-md backdrop-blur",
className,
)}
onClick={handleScrollToBottom}
size="icon"
type="button"
variant="outline"
{...props}
>
<ArrowDownIcon className="size-4" />
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Icon-only action buttons are missing explicit accessible names.

Both buttons should expose stable labels for screen readers (aria-label and/or sr-only text).

Proposed fix
     <Button
@@
+      aria-label="Scroll to latest messages"
       onClick={handleScrollToBottom}
@@
     >
       <ArrowDownIcon className="size-4" />
+      <span className="sr-only">Scroll to latest messages</span>
     </Button>
@@
     <Button
@@
+      aria-label="Download conversation"
       onClick={handleDownload}
@@
     >
       {children ?? <DownloadIcon className="size-4" />}
+      <span className="sr-only">Download conversation</span>
     </Button>

Also applies to: 166-178

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

In `@src/components/ai-elements/conversation.tsx` around lines 96 - 108, The
icon-only Buttons in the Conversation component lack accessible names; update
the Button using handleScrollToBottom (and the similar Button at the 166-178
block) to include stable screen-reader text by adding an aria-label (e.g.,
aria-label="Scroll to bottom") and/or include an sr-only <span> with descriptive
text inside the Button to ensure screen readers get a clear label while
preserving the visual icon; make the change on the Button instances in
conversation.tsx that render ArrowDownIcon and the other icon-only control.

Comment on lines +75 to +99
export function MessageAction({
tooltip,
label,
className,
children,
variant = "ghost",
size = "icon",
...props
}: MessageActionProps) {
const button = (
<Button
className={cn("size-8 rounded-md", className)}
size={size}
type="button"
variant={variant}
{...props}
>
{children}
<span className="sr-only">{label ?? tooltip}</span>
</Button>
);

if (!tooltip) {
return button;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Icon action can be inaccessible when no label/tooltip is passed.

Line 93 can render an empty sr-only node, which leaves the button without a reliable accessible name.

Proposed fix
 export function MessageAction({
   tooltip,
   label,
@@
 }: MessageActionProps) {
+  const accessibleLabel = label ?? tooltip ?? "Message action";
+
   const button = (
     <Button
       className={cn("size-8 rounded-md", className)}
+      aria-label={accessibleLabel}
       size={size}
       type="button"
       variant={variant}
       {...props}
     >
       {children}
-      <span className="sr-only">{label ?? tooltip}</span>
+      <span className="sr-only">{accessibleLabel}</span>
     </Button>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ai-elements/message.tsx` around lines 75 - 99, MessageAction
currently renders an empty <span className="sr-only"> when neither label nor
tooltip is provided, leaving the Button without an accessible name; fix by
computing a single accessibleName = label ?? tooltip ?? "Action", pass that
value as aria-label to the Button and only render the sr-only span when
accessibleName is truthy (so you never render an empty sr-only node), updating
references in the MessageAction function and ensuring the Button receives
aria-label={accessibleName}.

Comment on lines +113 to +127
export const MessageResponse = memo(
({ className, ...props }: MessageResponseProps) => (
<Streamdown
className={cn(
"size-full break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
className,
)}
components={streamdownComponents}
{...props}
/>
),
(prevProps, nextProps) =>
prevProps.children === nextProps.children &&
prevProps.isAnimating === nextProps.isAnimating,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify where MessageResponse receives props beyond `children`/`isAnimating`.
rg -n --type=tsx '<MessageResponse\b' -C3
rg -n --type=tsx '\bclassName=|\bcomponents=|\bremarkPlugins=|\brehypePlugins=' -C2

Repository: prudentbird/fuseion

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Search for MessageResponse component usage - corrected file type flag
rg -n 'MessageResponse' -A 3 -B 1

Repository: prudentbird/fuseion

Length of output: 2895


🏁 Script executed:

#!/bin/bash
# Check the actual file to understand the component signature and usage
cat -n src/components/ai-elements/message.tsx | head -150

Repository: prudentbird/fuseion

Length of output: 4369


Custom memo comparator prevents className updates.

The component is used with dynamic className values (e.g., src/components/chat/messages/message.tsx:92-101), but the comparator at lines 124-126 only checks children and isAnimating. Changes to className or other StreamdownProps won't trigger re-renders.

Remove the custom comparator to revert to React's default shallow comparison:

Fix
 export const MessageResponse = memo(
   ({ className, ...props }: MessageResponseProps) => (
     <Streamdown
       className={cn(
         "size-full break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
         className,
       )}
       components={streamdownComponents}
       {...props}
     />
-  ),
-  (prevProps, nextProps) =>
-    prevProps.children === nextProps.children &&
-    prevProps.isAnimating === nextProps.isAnimating,
+  ),
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const MessageResponse = memo(
({ className, ...props }: MessageResponseProps) => (
<Streamdown
className={cn(
"size-full break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
className,
)}
components={streamdownComponents}
{...props}
/>
),
(prevProps, nextProps) =>
prevProps.children === nextProps.children &&
prevProps.isAnimating === nextProps.isAnimating,
);
export const MessageResponse = memo(
({ className, ...props }: MessageResponseProps) => (
<Streamdown
className={cn(
"size-full break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
className,
)}
components={streamdownComponents}
{...props}
/>
),
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ai-elements/message.tsx` around lines 113 - 127, The custom
memo comparator on MessageResponse is preventing updates when className or other
Streamdown props change; remove the second argument (the comparator function)
from the memo(...) call so React uses its default shallow comparison, ensuring
changes to className, children, isAnimating, or other props (passed into
Streamdown via {...props}) will trigger re-renders of MessageResponse (symbols:
MessageResponse, Streamdown, streamdownComponents, className, props).

Comment on lines +107 to +117
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
onKeyDown?.(event);

if (event.defaultPrevented) {
return;
}

if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
event.preventDefault();
event.currentTarget.form?.requestSubmit();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't let Enter bypass the stop state.

requestSubmit() always takes the form down the submit path. In the current composition, src/components/chat/input.tsx Lines 60-64 put the button into stop mode while status is generating, so pressing Enter here can fire another send instead of stop(). Please gate the Enter shortcut on the generating state, or give callers a way to disable Enter-to-submit when a stop button is active.

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

In `@src/components/ai-elements/prompt-input.tsx` around lines 107 - 117, The
Enter key handler in handleKeyDown currently always calls form.requestSubmit(),
which bypasses the stop state; update handleKeyDown to respect a generating/stop
state (e.g., an isGenerating or disableEnterSubmit prop passed from the parent)
and when that state is true either prevent submission and invoke the stop action
or ignore the Enter key entirely instead of calling requestSubmit(); ensure the
parent (chat input) can pass the generating flag so the stop button behavior in
src/components/chat/input.tsx is honored and Enter cannot trigger a new send
while generating.

Comment on lines +65 to +71
const isExplicitlyClosed = defaultOpen === false;

const [isOpen, setIsOpen] = useControllableState<boolean>({
defaultProp: resolvedDefaultOpen,
onChange: onOpenChange,
prop: open,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether Reasoning is used in controlled mode and where lifecycle behavior matters.
rg -n --type=tsx '<Reasoning\b' -C3
rg -n --type=tsx '\bopen=|\bdefaultOpen=|\bonOpenChange=' -C3

Repository: prudentbird/fuseion

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Find the reasoning.tsx file and read the relevant sections
fd -e tsx -e ts | grep -i reasoning

# Also list tsx files to understand what's available
fd -e tsx | head -20

Repository: prudentbird/fuseion

Length of output: 755


🏁 Script executed:

#!/bin/bash
# Search for Reasoning component usage without -t flag
rg '<Reasoning' -A3 -B3 | head -100

Repository: prudentbird/fuseion

Length of output: 1694


🏁 Script executed:

#!/bin/bash
# Look for open, defaultOpen, onOpenChange usage across TSX files
rg 'open=|defaultOpen=|onOpenChange=' -A2 -B2 | head -150

Repository: prudentbird/fuseion

Length of output: 1460


🏁 Script executed:

#!/bin/bash
# Check the size of reasoning.tsx and then read it
wc -l src/components/ai-elements/reasoning.tsx

Repository: prudentbird/fuseion

Length of output: 107


🏁 Script executed:

#!/bin/bash
# Read the reasoning.tsx file with line numbers
cat -n src/components/ai-elements/reasoning.tsx

Repository: prudentbird/fuseion

Length of output: 6689


Open-state lifecycle has two confirmed regressions: controlled-close override and auto-close reset failure.

  • Line 65: isExplicitlyClosed only checks defaultOpen === false, ignoring controlled open={false} as an explicit close signal. The effect at line 95 will force-open the component during streaming even if explicitly closed via the open prop.
  • Auto-close one-shot failure: hasAutoClosed is never reset when a new stream cycle begins. Once triggered on the first stream, the auto-close effect (lines 100–114) will not run again because the condition !hasAutoClosed remains false for all subsequent cycles.
Proposed fix
-  const isExplicitlyClosed = defaultOpen === false;
+  const isExplicitlyClosed = open === false || defaultOpen === false;
@@
   useEffect(() => {
     if (isStreaming) {
       hasEverStreamedRef.current = true;
+      setHasAutoClosed(false);
 
       if (startTimeRef.current === null) {
         startTimeRef.current = Date.now();
       }

Also applies to: 77–114

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

In `@src/components/ai-elements/reasoning.tsx` around lines 65 - 71, The
component's explicit-close detection and auto-close lifecycle are broken: update
the explicit-close check to consider both the controlled prop and default (e.g.,
compute isExplicitlyClosed using resolvedDefaultOpen === false || open === false
so the controlled open={false} prevents forced opens), ensure the auto-open
effect that toggles isOpen during streaming (the effect that watches
streaming/streamId) respects isExplicitlyClosed before forcing open, and add
logic to reset hasAutoClosed (setHasAutoClosed(false)) whenever a new stream
cycle begins (watch streaming or streamId) so the one-shot auto-close can run
again on subsequent streams; these changes involve isExplicitlyClosed,
open/resolvedDefaultOpen, useControllableState/isOpen, hasAutoClosed and the
streaming-related effects.

Comment on lines +7 to +20
export function Shimmer({
children,
className,
duration = 1.8,
}: React.ComponentProps<"span"> & {
duration?: number;
}) {
return (
<span
className={cn(
"inline-block bg-[linear-gradient(110deg,currentColor,rgba(255,255,255,0.35),currentColor)] bg-[length:200%_100%] bg-clip-text text-transparent animate-[shimmer_1.8s_linear_infinite]",
className,
)}
style={{ animationDuration: `${duration}s` }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Forward the rest of the span props.

This signature advertises ComponentProps<"span">, but the implementation only passes className and its own inline style. Any caller aria-*, data-*, handlers, or style values are silently dropped.

♻️ Minimal fix
 export function Shimmer({
   children,
   className,
   duration = 1.8,
+  style,
+  ...props
 }: React.ComponentProps<"span"> & {
   duration?: number;
 }) {
   return (
     <span
       className={cn(
         "inline-block bg-[linear-gradient(110deg,currentColor,rgba(255,255,255,0.35),currentColor)] bg-[length:200%_100%] bg-clip-text text-transparent animate-[shimmer_1.8s_linear_infinite]",
         className,
       )}
-      style={{ animationDuration: `${duration}s` }}
+      {...props}
+      style={{ ...style, animationDuration: `${duration}s` }}
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ai-elements/shimmer.tsx` around lines 7 - 20, The Shimmer
component claims React.ComponentProps<"span"> but currently drops all other span
props; update Shimmer to collect the rest props (e.g., function Shimmer({
children, className, duration = 1.8, ...props }: React.ComponentProps<"span"> &
{ duration?: number }) and spread them onto the <span> so aria-*, data-*, event
handlers, etc. are forwarded. Keep merging className via the existing cn(...)
call and merge styles by combining props.style with the computed
animationDuration (e.g., { ...(props.style || {}), animationDuration:
`${duration}s` }) so external style keys are preserved while ensuring the
shimmer duration is applied.

@prudentbird
prudentbird merged commit bfe465e into dev Apr 3, 2026
4 of 5 checks passed
@prudentbird
prudentbird deleted the feat/ai-sdk-v6-ai-elements-next-16 branch April 3, 2026 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant