hasOutput && setIsOutputExpanded(open)}
+ open={hasOutput ? isOutputExpanded : false}
>
- {/* Header - fixed height to prevent layout shift */}
- {/* biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: interactive tool header */}
-
- hasMoreOutput && !isPending && setIsOutputExpanded(!isOutputExpanded)
- }
- >
-
- {isPending ? "Running command: " : "Ran command: "}
- {commandSummary}
-
-
- {/* Status and expand */}
-
- {!isPending && (
-
- {isSuccess && (
- <>
-
- Success
- >
- )}
- {isError && (
- <>
-
- Failed
- >
- )}
-
+
+
-
- {/* Content - always visible */}
- {/* biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: clickable to expand */}
-
- hasMoreOutput && !isOutputExpanded && setIsOutputExpanded(true)
- }
- >
- {/* Command */}
- {command && (
-
-
$
-
- {command}
-
+ {/* Status */}
+
+ {!isPending && (
+
+ {isSuccess && (
+ <>
+
+ Success
+ >
+ )}
+ {isError && (
+ <>
+
+ Failed
+ >
+ )}
+
+ )}
+
+ {isPending && }
+
- )}
+
+
- {/* Stdout */}
- {stdout && (
-
- {isOutputExpanded ? stdout : stdoutLimited.text}
-
- )}
+ {hasOutput && (
+
+
+ {/* Command */}
+ {command && (
+
+ $
+
+ {command}
+
+
+ )}
- {/* Stderr */}
- {stderr && (
-
+ {stdout}
+
+ )}
+
+ {/* Stderr */}
+ {stderr && (
+
+ {stderr}
+
)}
- >
- {isOutputExpanded ? stderr : stderrLimited.text}
- )}
-
-
+
+ )}
+
);
};
diff --git a/packages/ui/src/components/ai-elements/file-diff-tool.tsx b/packages/ui/src/components/ai-elements/file-diff-tool.tsx
index bfa57941d09..7a84b7988b3 100644
--- a/packages/ui/src/components/ai-elements/file-diff-tool.tsx
+++ b/packages/ui/src/components/ai-elements/file-diff-tool.tsx
@@ -3,7 +3,7 @@
import { FileCode2Icon } from "lucide-react";
import { useMemo, useState } from "react";
import { cn } from "../../lib/utils";
-import { Shimmer } from "./shimmer";
+import { ShimmerLabel } from "./shimmer-label";
type FileDiffToolState =
| "input-streaming"
@@ -143,13 +143,9 @@ export const FileDiffTool = ({
{isStreaming && !filePath ? (
-
+
{isWriteMode ? "Writing file..." : "Editing file..."}
-
+
) : (
{isWriteMode ? "Wrote" : "Edited"}{" "}
diff --git a/packages/ui/src/components/ai-elements/message.tsx b/packages/ui/src/components/ai-elements/message.tsx
index c831aa805eb..669841d2a8b 100644
--- a/packages/ui/src/components/ai-elements/message.tsx
+++ b/packages/ui/src/components/ai-elements/message.tsx
@@ -22,6 +22,12 @@ import {
} from "../ui/tooltip";
const streamdownPlugins = { mermaid };
+const defaultMessageAnimation = {
+ animation: "blurIn",
+ sep: "char",
+ duration: 180,
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
+} as const;
export type MessageProps = HTMLAttributes & {
from: UIMessage["role"];
@@ -31,7 +37,7 @@ export const Message = ({ className, from, ...props }: MessageProps) => (
(
& {
- from: UIMessage["role"];
-};
+export type MessageBranchSelectorProps = ComponentProps
;
export const MessageBranchSelector = ({
className,
- from,
...props
}: MessageBranchSelectorProps) => {
const { totalBranches } = useMessageBranch();
@@ -229,7 +233,10 @@ export const MessageBranchSelector = ({
return (
*:first-child]:rounded-l-md [&>*:last-child]:rounded-r-md",
+ className,
+ )}
orientation="horizontal"
{...props}
/>
@@ -263,7 +270,6 @@ export type MessageBranchNextProps = ComponentProps;
export const MessageBranchNext = ({
children,
- className,
...props
}: MessageBranchNextProps) => {
const { goToNext, totalBranches } = useMessageBranch();
@@ -309,12 +315,13 @@ export type MessageResponseProps = ComponentProps;
export const MessageResponse = memo(
({ className, animated, isAnimating, ...props }: MessageResponseProps) => (
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_ol]:list-outside [&_ol]:pl-6 [&_ul]:list-outside [&_ul]:pl-6 [&_:not(pre)>code]:break-all",
className,
)}
isAnimating={isAnimating}
+ mode="streaming"
plugins={isAnimating ? undefined : streamdownPlugins}
{...props}
/>
diff --git a/packages/ui/src/components/ai-elements/plan.tsx b/packages/ui/src/components/ai-elements/plan.tsx
index 64eaf7143bd..cff62af6b5c 100644
--- a/packages/ui/src/components/ai-elements/plan.tsx
+++ b/packages/ui/src/components/ai-elements/plan.tsx
@@ -74,7 +74,7 @@ export const PlanTitle = ({ children, ...props }: PlanTitleProps) => {
return (
- {isStreaming ? {children} : children}
+ {isStreaming ? {children} : children}
);
};
@@ -99,7 +99,7 @@ export const PlanDescription = ({
data-slot="plan-description"
{...props}
>
- {isStreaming ? {children} : children}
+ {isStreaming ? {children} : children}
);
};
diff --git a/packages/ui/src/components/ai-elements/queue.tsx b/packages/ui/src/components/ai-elements/queue.tsx
index fa33738eeb4..e3a80cab2fa 100644
--- a/packages/ui/src/components/ai-elements/queue.tsx
+++ b/packages/ui/src/components/ai-elements/queue.tsx
@@ -198,7 +198,7 @@ export type QueueSectionProps = ComponentProps;
export const QueueSection = ({
className,
- defaultOpen = true,
+ defaultOpen = false,
...props
}: QueueSectionProps) => (
diff --git a/packages/ui/src/components/ai-elements/reasoning.tsx b/packages/ui/src/components/ai-elements/reasoning.tsx
index 0a67fc55281..57de9622cc9 100644
--- a/packages/ui/src/components/ai-elements/reasoning.tsx
+++ b/packages/ui/src/components/ai-elements/reasoning.tsx
@@ -11,7 +11,7 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
-import { Shimmer } from "./shimmer";
+import { ShimmerLabel } from "./shimmer-label";
type ReasoningContextValue = {
isStreaming: boolean;
@@ -119,7 +119,11 @@ export type ReasoningTriggerProps = ComponentProps<
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
if (isStreaming || duration === 0) {
- return Thinking...;
+ return (
+
+ Thinking...
+
+ );
}
if (duration === undefined) {
return Thought for a few seconds
;
diff --git a/packages/ui/src/components/ai-elements/shimmer-label.tsx b/packages/ui/src/components/ai-elements/shimmer-label.tsx
new file mode 100644
index 00000000000..7537748bfae
--- /dev/null
+++ b/packages/ui/src/components/ai-elements/shimmer-label.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { cn } from "../../lib/utils";
+import type { TextShimmerProps } from "./shimmer";
+import { Shimmer } from "./shimmer";
+
+export type ShimmerLabelProps = Omit<
+ TextShimmerProps,
+ "children" | "className"
+> & {
+ children: string;
+ className?: string;
+ shimmerClassName?: string;
+ isShimmering?: boolean;
+};
+
+export const ShimmerLabel = ({
+ children,
+ className,
+ shimmerClassName,
+ isShimmering = true,
+ ...props
+}: ShimmerLabelProps) => (
+
+ {isShimmering ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+
+);
diff --git a/packages/ui/src/components/ai-elements/shimmer.tsx b/packages/ui/src/components/ai-elements/shimmer.tsx
index ec12b15252a..84047394c5b 100644
--- a/packages/ui/src/components/ai-elements/shimmer.tsx
+++ b/packages/ui/src/components/ai-elements/shimmer.tsx
@@ -16,14 +16,16 @@ export type TextShimmerProps = {
className?: string;
duration?: number;
spread?: number;
+ variant?: "tool" | "text";
};
const ShimmerComponent = ({
children,
- as: Component = "p",
+ as: Component = "span",
className,
duration = 2,
spread = 2,
+ variant = "tool",
}: TextShimmerProps) => {
const MotionComponent = motion.create(
Component as keyof JSX.IntrinsicElements,
@@ -38,9 +40,12 @@ const ShimmerComponent = ({
(
export type TaskProps = ComponentProps;
export const Task = ({
- defaultOpen = true,
+ defaultOpen = false,
className,
...props
}: TaskProps) => (
diff --git a/packages/ui/src/components/ai-elements/tool-call.tsx b/packages/ui/src/components/ai-elements/tool-call.tsx
index 631931c9dd1..765afaa4126 100644
--- a/packages/ui/src/components/ai-elements/tool-call.tsx
+++ b/packages/ui/src/components/ai-elements/tool-call.tsx
@@ -2,7 +2,7 @@
import type { ComponentType } from "react";
import { cn } from "../../lib/utils";
-import { Shimmer } from "./shimmer";
+import { ShimmerLabel } from "./shimmer-label";
export type ToolCallProps = {
icon: ComponentType<{ className?: string }>;
@@ -33,19 +33,7 @@ export const ToolCall = ({
>
-
- {isPending ? (
-
- {title}
-
- ) : (
- title
- )}
-
+
{title}
{subtitle && (
// biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: clickable subtitle
;
export const Tool = ({ className, ...props }: ToolProps) => (
);
@@ -53,37 +57,20 @@ function getToolDisplayName(title?: string, type?: string): string {
return "tool";
}
-const getStatusBadge = (status: ToolDisplayState) => {
- const labels: Record = {
- "awaiting-input": "Pending",
- "input-streaming": "Pending",
- "input-complete": "Running",
- "input-available": "Running",
- "approval-requested": "Awaiting Approval",
- "approval-responded": "Responded",
- "output-available": "Completed",
- "output-error": "Error",
- "output-denied": "Denied",
- };
-
+const getStatusIcon = (status: ToolDisplayState) => {
const icons: Record = {
- "awaiting-input": ,
- "input-streaming": ,
- "input-complete": ,
- "input-available": ,
- "approval-requested": ,
- "approval-responded": ,
- "output-available": ,
- "output-error": ,
- "output-denied": ,
+ "awaiting-input": ,
+ "input-streaming": ,
+ "input-complete": ,
+ "input-available": ,
+ "approval-requested": ,
+ "approval-responded": ,
+ "output-available": ,
+ "output-error": ,
+ "output-denied": ,
};
- return (
-
- {icons[status]}
- {labels[status]}
-
- );
+ return icons[status];
};
export const ToolHeader = ({
@@ -95,19 +82,21 @@ export const ToolHeader = ({
}: ToolHeaderProps) => (
-
-
-
+
+
+
{getToolDisplayName(title, type)}
- {getStatusBadge(state)}
-
+
+ {getStatusIcon(state)}
+
+
);
@@ -116,7 +105,7 @@ export type ToolContentProps = ComponentProps;
export const ToolContent = ({ className, ...props }: ToolContentProps) => (
{isPending ? (
-
+
Fetching
-
+
) : (
Fetched
)}
diff --git a/packages/ui/src/components/ai-elements/web-search-tool.tsx b/packages/ui/src/components/ai-elements/web-search-tool.tsx
index 645426f25c8..19ffe63b6e1 100644
--- a/packages/ui/src/components/ai-elements/web-search-tool.tsx
+++ b/packages/ui/src/components/ai-elements/web-search-tool.tsx
@@ -4,7 +4,7 @@ import { ExternalLinkIcon, SearchIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "../../lib/utils";
import { Loader } from "./loader";
-import { Shimmer } from "./shimmer";
+import { ShimmerLabel } from "./shimmer-label";
type WebSearchToolState =
| "input-streaming"
@@ -55,13 +55,9 @@ export const WebSearchTool = ({
{isPending ? (
-
+
Searching
-
+
) : (
Searched
)}