feat(chat): migrate to AI SDK v6 and AI Elements - #24
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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>
| components={streamdownComponents} | ||
| {...props} | ||
| /> | ||
| ), |
There was a problem hiding this comment.
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>
| ); | ||
| } | ||
|
|
||
| export type ConversationScrollButtonProps = ComponentProps<typeof Button>; |
There was a problem hiding this comment.
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>
| duration?: number; | ||
| }) { | ||
| return ( | ||
| <span |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
src/components/ui/collapsible.tsx (1)
4-29: Verify the Radix Collapsible import shape.This wrapper aliases
Collapsiblefrom"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: Avoidlatestforradix-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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (13)
next.config.tspackage.jsonsrc/components/ai-elements/conversation.tsxsrc/components/ai-elements/message.tsxsrc/components/ai-elements/prompt-input.tsxsrc/components/ai-elements/reasoning.tsxsrc/components/ai-elements/shimmer.tsxsrc/components/ai-elements/streamdown.tsxsrc/components/chat/index.tsxsrc/components/chat/input.tsxsrc/components/chat/messages/index.tsxsrc/components/chat/messages/message.tsxsrc/components/ui/collapsible.tsx
| <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> |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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}.
| 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, | ||
| ); |
There was a problem hiding this comment.
🧩 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=' -C2Repository: 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 1Repository: 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 -150Repository: 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.
| 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).
| 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(); | ||
| } |
There was a problem hiding this comment.
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.
| const isExplicitlyClosed = defaultOpen === false; | ||
|
|
||
| const [isOpen, setIsOpen] = useControllableState<boolean>({ | ||
| defaultProp: resolvedDefaultOpen, | ||
| onChange: onOpenChange, | ||
| prop: open, | ||
| }); |
There was a problem hiding this comment.
🧩 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=' -C3Repository: 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 -20Repository: prudentbird/fuseion
Length of output: 755
🏁 Script executed:
#!/bin/bash
# Search for Reasoning component usage without -t flag
rg '<Reasoning' -A3 -B3 | head -100Repository: 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 -150Repository: 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.tsxRepository: 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.tsxRepository: prudentbird/fuseion
Length of output: 6689
Open-state lifecycle has two confirmed regressions: controlled-close override and auto-close reset failure.
- Line 65:
isExplicitlyClosedonly checksdefaultOpen === false, ignoring controlledopen={false}as an explicit close signal. The effect at line 95 will force-open the component during streaming even if explicitly closed via theopenprop. - Auto-close one-shot failure:
hasAutoClosedis 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!hasAutoClosedremains 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.
| 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` }} |
There was a problem hiding this comment.
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.
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
Conversation,Message,PromptInput,Reasoning, andShimmer.streamdownwith inline and block code styling.Refactors
logging.browserToTerminal, removed the experimental browser debug flag, dropped Turbopack fromdev/build/preview, and switched code-quality topnpm.@ai-sdk/*packages, integratedstreamdown, and added a Radix-basedui/collapsiblewrapper.Written for commit 481a3f3. Summary will update on new commits.
Summary by CodeRabbit
New Features
Dependencies