+ {/* Placeholder keeps layout stable while overlay is shown */}
+
+ { if (e.target === e.currentTarget) setEnlarged(false); }}
+ >
+
+
+
+
+
+
+
+ >
+ );
+ }
+
+ return (
+
@@ -249,6 +352,7 @@ function StreamdownBlock(props: BlockProps) {
if (codeFence) {
const svgSource = !props.isIncomplete && isSvgFence(codeFence) ? sanitizeSvg(codeFence.source) : null;
+ const htmlSource = !props.isIncomplete && isHtmlFence(codeFence) ? codeFence.source : null;
return (
<>
@@ -260,6 +364,7 @@ function StreamdownBlock(props: BlockProps) {
/>
{svgSource &&
}
+ {htmlSource &&
}
>
);
}
diff --git a/studio/frontend/src/components/assistant-ui/reasoning.tsx b/studio/frontend/src/components/assistant-ui/reasoning.tsx
index 12d5799a16..4f3f8075a4 100644
--- a/studio/frontend/src/components/assistant-ui/reasoning.tsx
+++ b/studio/frontend/src/components/assistant-ui/reasoning.tsx
@@ -19,10 +19,11 @@ import {
useAuiState,
useScrollLock,
} from "@assistant-ui/react";
+import { copyToClipboard } from "@/lib/copy-to-clipboard";
import { Idea01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { type VariantProps, cva } from "class-variance-authority";
-import { ChevronDownIcon } from "lucide-react";
+import { ChevronDownIcon, CopyIcon, CheckIcon } from "lucide-react";
import {
type CSSProperties,
type ComponentProps,
@@ -263,6 +264,45 @@ function ReasoningText({
const ReasoningImpl: ReasoningMessagePartComponent = () =>
;
+const COPY_RESET_MS = 2000;
+
+function ReasoningCopyButton({ startIndex, endIndex }: { startIndex: number; endIndex: number }) {
+ const [copied, setCopied] = useState(false);
+ const resetRef = useRef
| null>(null);
+
+ const reasoningText = useAuiState(({ message }) => {
+ return message.parts
+ .slice(startIndex, endIndex + 1)
+ .filter((p) => p.type === "reasoning")
+ .map((p) => ("text" in p ? (p as { text: string }).text : ""))
+ .join("\n");
+ });
+
+ const handleCopy = useCallback(() => {
+ if (copyToClipboard(reasoningText)) {
+ setCopied(true);
+ if (resetRef.current) clearTimeout(resetRef.current);
+ resetRef.current = setTimeout(() => setCopied(false), COPY_RESET_MS);
+ }
+ }, [reasoningText]);
+
+ return (
+
+ );
+}
+
const ReasoningGroupImpl: ReasoningGroupComponent = ({
children,
startIndex,
@@ -328,10 +368,15 @@ const ReasoningGroupImpl: ReasoningGroupComponent = ({
onOpenChange={handleOpenChange}
variant={variant}
>
-
+
+
+ {isOpen && !isReasoningStreaming && (
+
+ )}
+
{
+ try {
+ return new URL(url).hostname.replace(/^www\./, "");
+ } catch {
+ return url;
+ }
+};
+
+const getDomainInitial = (url: string): string => {
+ const domain = extractDomain(url);
+ return domain.charAt(0).toUpperCase();
+};
+
+function SourceIcon({
+ url,
+ className,
+ ...props
+}: ComponentProps<"span"> & { url: string }) {
+ const [hasError, setHasError] = useState(false);
+ const domain = extractDomain(url);
+
+ if (hasError) {
+ return (
+
+ {getDomainInitial(url)}
+
+ );
+ }
+
+ return (
+
setHasError(true)}
+ {...(props as ComponentProps<"img">)}
+ />
+ );
+}
+
+function SourceTitle({ className, ...props }: ComponentProps<"span">) {
+ return (
+
+ );
+}
+
+export type SourceProps = Omit &
+ ComponentProps<"a"> & {
+ asChild?: boolean;
+ };
+
+function Source({
+ className,
+ variant,
+ size,
+ asChild = false,
+ target = "_blank",
+ rel = "noopener noreferrer",
+ ...props
+}: SourceProps) {
+ return (
+
+ )}
+ />
+
+ );
+}
+
+const SourcesImpl: SourceMessagePartComponent = ({
+ url,
+ title,
+ sourceType,
+}) => {
+ if (sourceType !== "url" || !url) return null;
+
+ const domain = extractDomain(url);
+ const displayTitle = title || domain;
+
+ return (
+
+
+
+ {displayTitle}
+
+
+ );
+};
+
+const Sources = memo(SourcesImpl) as unknown as SourceMessagePartComponent & {
+ Root: typeof Source;
+ Icon: typeof SourceIcon;
+ Title: typeof SourceTitle;
+};
+
+Sources.displayName = "Sources";
+Sources.Root = Source;
+Sources.Icon = SourceIcon;
+Sources.Title = SourceTitle;
+
+export {
+ Sources,
+ Source,
+ SourceIcon,
+ SourceTitle,
+ badgeVariants as sourceVariants,
+};
diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx
index a16d1f3c2e..567c25b53f 100644
--- a/studio/frontend/src/components/assistant-ui/thread.tsx
+++ b/studio/frontend/src/components/assistant-ui/thread.tsx
@@ -9,7 +9,12 @@ import {
import { MessageTiming } from "@/components/assistant-ui/message-timing";
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
import { Reasoning, ReasoningGroup } from "@/components/assistant-ui/reasoning";
+import { Sources } from "@/components/assistant-ui/sources";
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
+import { ToolGroup } from "@/components/assistant-ui/tool-group";
+import { WebSearchToolUI } from "@/components/assistant-ui/tool-ui-web-search";
+import { PythonToolUI } from "@/components/assistant-ui/tool-ui-python";
+import { TerminalToolUI } from "@/components/assistant-ui/tool-ui-terminal";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { Button } from "@/components/ui/button";
import { sentAudioNames } from "@/features/chat/api/chat-adapter";
@@ -52,7 +57,7 @@ import {
TerminalIcon,
XIcon,
} from "lucide-react";
-import { type FC, useCallback, useRef, useState } from "react";
+import { type FC, useCallback, useEffect, useRef, useState } from "react";
import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store";
export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({
@@ -384,6 +389,20 @@ const CodeToolsToggle: FC = () => {
const ToolStatusDisplay: FC = () => {
const toolStatus = useChatRuntimeStore((s) => s.toolStatus);
+ const [elapsed, setElapsed] = useState(0);
+
+ useEffect(() => {
+ if (!toolStatus) {
+ setElapsed(0);
+ return;
+ }
+ setElapsed(0);
+ const interval = setInterval(() => {
+ setElapsed((prev) => prev + 1);
+ }, 1000);
+ return () => clearInterval(interval);
+ }, [toolStatus]);
+
if (!toolStatus) return null;
const isRunning = toolStatus.startsWith("Running");
const StatusIcon = isRunning ? TerminalIcon : GlobeIcon;
@@ -392,6 +411,7 @@ const ToolStatusDisplay: FC = () => {
{toolStatus}
+ {elapsed}s
);
@@ -485,7 +505,16 @@ const AssistantMessage: FC = () => {
Text: MarkdownText,
Reasoning: Reasoning,
ReasoningGroup: ReasoningGroup,
- tools: { Fallback: ToolFallback },
+ Source: Sources,
+ ToolGroup: ToolGroup,
+ tools: {
+ by_name: {
+ web_search: WebSearchToolUI,
+ python: PythonToolUI,
+ terminal: TerminalToolUI,
+ },
+ Fallback: ToolFallback,
+ },
}}
/>