diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/MastraToolCallBlock.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/MastraToolCallBlock.tsx index 9558155927a..71f2101e3f3 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/MastraToolCallBlock.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/MastraToolCallBlock.tsx @@ -1,12 +1,5 @@ import { BashTool } from "@superset/ui/ai-elements/bash-tool"; import { FileDiffTool } from "@superset/ui/ai-elements/file-diff-tool"; -import { - Tool, - ToolContent, - ToolHeader, - ToolInput, - ToolOutput, -} from "@superset/ui/ai-elements/tool"; import { ToolCall } from "@superset/ui/ai-elements/tool-call"; import { UserQuestionTool } from "@superset/ui/ai-elements/user-question-tool"; import { WebFetchTool } from "@superset/ui/ai-elements/web-fetch-tool"; @@ -15,13 +8,9 @@ import { getToolName } from "ai"; import { FileIcon, FolderIcon, MessageCircleQuestionIcon } from "lucide-react"; import { READ_ONLY_TOOLS } from "../../constants"; import type { ToolPart } from "../../utils/tool-helpers"; -import { - getArgs, - getResult, - toToolDisplayState, - toWsToolState, -} from "../../utils/tool-helpers"; +import { getArgs, getResult, toWsToolState } from "../../utils/tool-helpers"; import { ReadOnlyToolCall } from "../ReadOnlyToolCall"; +import { GenericToolCall } from "./components/GenericToolCall"; interface MastraToolCallBlockProps { part: ToolPart; @@ -40,7 +29,16 @@ export function MastraToolCallBlock({ // --- Execute command → BashTool --- if (toolName === "mastra_workspace_execute_command") { const command = String(args.command ?? args.cmd ?? ""); - const stdout = result.stdout != null ? String(result.stdout) : undefined; + const stdout = + result.stdout != null + ? String(result.stdout) + : result.output != null + ? typeof result.output === "string" + ? result.output + : JSON.stringify(result.output, null, 2) + : result.text != null + ? String(result.text) + : undefined; const stderr = result.stderr != null ? String(result.stderr) : undefined; const exitCode = result.exitCode != null ? Number(result.exitCode) : undefined; @@ -192,28 +190,5 @@ export function MastraToolCallBlock({ } // --- Fallback: generic tool UI --- - const output = - "output" in part ? (part as { output: unknown }).output : undefined; - const isError = part.state === "output-error"; - - return ( - - - - {part.input != null && } - {(output != null || isError) && ( - - )} - - - ); + return ; } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/GenericToolCall.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/GenericToolCall.tsx new file mode 100644 index 00000000000..2d6f67af5be --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/GenericToolCall.tsx @@ -0,0 +1,34 @@ +import { + Tool, + ToolContent, + ToolHeader, + ToolInput, + ToolOutput, +} from "@superset/ui/ai-elements/tool"; +import type { ToolPart } from "../../../../utils/tool-helpers"; +import { getGenericToolCallState } from "./getGenericToolCallState"; + +type GenericToolCallProps = { + part: ToolPart; + toolName: string; +}; + +export function GenericToolCall({ part, toolName }: GenericToolCallProps) { + const { output, isError, displayState, errorText } = + getGenericToolCallState(part); + + return ( + + + + {part.input != null && } + {(output != null || isError) && ( + + )} + + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/getGenericToolCallState.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/getGenericToolCallState.ts new file mode 100644 index 00000000000..8ae55c160d8 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/getGenericToolCallState.ts @@ -0,0 +1,57 @@ +import type { ToolDisplayState } from "@superset/ui/ai-elements/tool"; +import type { ToolPart } from "../../../../utils/tool-helpers"; +import { toToolDisplayState } from "../../../../utils/tool-helpers"; + +export type GenericToolCallState = { + output: unknown; + isError: boolean; + displayState: ToolDisplayState; + errorText?: string; +}; + +function stringifyValue(value: unknown): string { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +export function getGenericToolCallState(part: ToolPart): GenericToolCallState { + const output = + "output" in part ? (part as { output: unknown }).output : undefined; + const outputObject = + output != null && typeof output === "object" + ? (output as Record) + : undefined; + const outputError = outputObject?.error; + const isOutputError = + outputObject != null && "error" in outputObject && Boolean(outputError); + const isError = part.state === "output-error" || isOutputError; + + const baseDisplayState = toToolDisplayState(part); + const displayState = + isOutputError && baseDisplayState === "output-available" + ? "output-error" + : baseDisplayState; + + let errorText: string | undefined; + if (isError) { + if (typeof output === "string") { + errorText = output; + } else if (typeof outputError === "string") { + errorText = outputError; + } else if (typeof outputObject?.message === "string") { + errorText = outputObject.message; + } else if (outputError !== undefined) { + errorText = stringifyValue(outputError); + } + } + + return { + output, + isError, + displayState, + errorText, + }; +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/index.ts new file mode 100644 index 00000000000..c44d4e2d968 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MastraToolCallBlock/components/GenericToolCall/index.ts @@ -0,0 +1,2 @@ +export { GenericToolCall } from "./GenericToolCall"; +export { getGenericToolCallState } from "./getGenericToolCallState"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessageList/MessageList.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessageList/MessageList.tsx index 2d1d7e856c2..b1ac34397b5 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessageList/MessageList.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessageList/MessageList.tsx @@ -5,7 +5,7 @@ import { ConversationScrollButton, } from "@superset/ui/ai-elements/conversation"; import { Message, MessageContent } from "@superset/ui/ai-elements/message"; -import { Shimmer } from "@superset/ui/ai-elements/shimmer"; +import { ShimmerLabel } from "@superset/ui/ai-elements/shimmer-label"; import type { ChatStatus, UIMessage } from "ai"; import { FileIcon, FileTextIcon, ImageIcon } from "lucide-react"; import { useCallback } from "react"; @@ -77,6 +77,8 @@ export function MessageList({ messages.map((msg, index) => { const isLastAssistant = msg.role === "assistant" && index === messages.length - 1; + const shouldAnimateStreaming = + isLastAssistant && (isStreaming || submitStatus === "submitted"); if (msg.role === "user") { const textContent = msg.parts @@ -144,17 +146,14 @@ export function MessageList({ {isLastAssistant && isThinking && msg.parts.length === 0 ? ( - + Thinking... - + ) : ( @@ -167,9 +166,9 @@ export function MessageList({ {isThinking && messages[messages.length - 1]?.role === "user" && ( - + Thinking... - + )} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx index a231883bc97..4d4e8884066 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/MessagePartsRenderer.tsx @@ -1,5 +1,4 @@ import { ExploringGroup } from "@superset/ui/ai-elements/exploring-group"; -import { MessageResponse } from "@superset/ui/ai-elements/message"; import type { UIMessage } from "ai"; import { getToolName, isToolUIPart } from "ai"; import { @@ -20,6 +19,7 @@ import { getArgs } from "../../utils/tool-helpers"; import { MastraToolCallBlock } from "../MastraToolCallBlock"; import { ReadOnlyToolCall } from "../ReadOnlyToolCall"; import { ReasoningBlock } from "../ReasoningBlock"; +import { StreamingMessageText } from "./components/StreamingMessageText"; interface MessagePartsRendererProps { parts: UIMessage["parts"]; @@ -96,14 +96,13 @@ export function MessagePartsRenderer({ if (part.type === "text") { nodes.push( - - {part.text} - , + />, ); i++; continue; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/components/StreamingMessageText/StreamingMessageText.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/components/StreamingMessageText/StreamingMessageText.tsx new file mode 100644 index 00000000000..fb362f53e3f --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/components/StreamingMessageText/StreamingMessageText.tsx @@ -0,0 +1,57 @@ +import { + MessageResponse, + type MessageResponseProps, +} from "@superset/ui/ai-elements/message"; +import { useEffect, useState } from "react"; + +const STREAM_TEXT_TICK_MS = 16; +const STREAM_TEXT_CHARS_PER_TICK = 2; + +interface StreamingMessageTextProps { + text: string; + isAnimating: boolean; + mermaid: MessageResponseProps["mermaid"]; + components?: MessageResponseProps["components"]; +} + +export function StreamingMessageText({ + text, + isAnimating, + mermaid, + components, +}: StreamingMessageTextProps) { + const [displayText, setDisplayText] = useState(text); + + useEffect(() => { + if (!isAnimating) { + setDisplayText(text); + return; + } + + setDisplayText((previous) => (text.startsWith(previous) ? previous : text)); + + const intervalId = window.setInterval(() => { + setDisplayText((previous) => { + if (previous.length >= text.length) return previous; + const nextLength = Math.min( + text.length, + previous.length + STREAM_TEXT_CHARS_PER_TICK, + ); + return text.slice(0, nextLength); + }); + }, STREAM_TEXT_TICK_MS); + + return () => window.clearInterval(intervalId); + }, [text, isAnimating]); + + return ( + + {displayText} + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/components/StreamingMessageText/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/components/StreamingMessageText/index.ts new file mode 100644 index 00000000000..f6d9625ae45 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/MessagePartsRenderer/components/StreamingMessageText/index.ts @@ -0,0 +1 @@ +export { StreamingMessageText } from "./StreamingMessageText"; diff --git a/packages/ui/src/components/ai-elements/bash-tool.tsx b/packages/ui/src/components/ai-elements/bash-tool.tsx index 3d3b2847bb5..2accf441247 100644 --- a/packages/ui/src/components/ai-elements/bash-tool.tsx +++ b/packages/ui/src/components/ai-elements/bash-tool.tsx @@ -1,10 +1,15 @@ "use client"; -import { CheckIcon, XIcon } from "lucide-react"; +import { CheckIcon, TerminalIcon, XIcon } from "lucide-react"; import { useMemo, useState } from "react"; import { cn } from "../../lib/utils"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "../ui/collapsible"; import { Loader } from "./loader"; -import { Shimmer } from "./shimmer"; +import { ShimmerLabel } from "./shimmer-label"; type BashToolState = | "input-streaming" @@ -33,21 +38,6 @@ function extractCommandSummary(command: string): string { return limited.join(", "); } -/** Limit text to N lines, returning whether it was truncated. */ -function limitLines( - text: string, - maxLines: number, -): { text: string; truncated: boolean } { - if (!text) return { text: "", truncated: false }; - const lines = text.split("\n"); - if (lines.length <= maxLines) { - return { text, truncated: false }; - } - return { text: lines.slice(0, maxLines).join("\n"), truncated: true }; -} - -const MAX_COLLAPSED_LINES = 3; - export const BashTool = ({ command, stdout, @@ -61,139 +51,113 @@ export const BashTool = ({ const isPending = state === "input-streaming" || state === "input-available"; const isSuccess = exitCode === 0; const isError = exitCode !== undefined && exitCode !== 0; - const _hasOutput = Boolean(stdout || stderr); - - const stdoutLimited = useMemo( - () => limitLines(stdout ?? "", MAX_COLLAPSED_LINES), - [stdout], - ); - const stderrLimited = useMemo( - () => limitLines(stderr ?? "", MAX_COLLAPSED_LINES), - [stderr], - ); - const hasMoreOutput = stdoutLimited.truncated || stderrLimited.truncated; const commandSummary = useMemo( () => (command ? extractCommandSummary(command) : ""), [command], ); - // Input still streaming - if (state === "input-streaming") { - return ( -
-
-
- - - Generating command - - -
-
-
- ); - } + const hasOutput = Boolean(command || stdout || stderr); return ( -
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 )}