import { IconAlertTriangle, IconArrowDown, IconCheck, IconChevronDown, IconCircleX, IconClock, IconExternalLink, IconLoader2, IconTool, } from "@tabler/icons-react"; import React from "react"; import ReactMarkdown, { defaultUrlTransform } from "react-markdown"; import remarkGfm from "remark-gfm"; import { ActionChatUiSurface } from "../chat/action-chat-ui-surface.js"; import { resolveToolRenderer } from "../chat/tool-render-registry.js"; import { resolveBuiltinActionChatRenderer, resolveBuiltinFallbackToolRenderer, isBuiltinDataWidgetActionRenderer, } from "../chat/widgets/builtin-tool-renderers.js"; import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, } from "../components/ui/message-scroller.js"; import { HighlightedCodeBlock as SharedHighlightedCodeBlock } from "../HighlightedCodeBlock.js"; import { McpAppRenderer } from "../mcp-apps/McpAppRenderer.js"; import { humanizeToolName } from "../tool-display.js"; import { cn } from "../utils.js"; import type { AgentConversationAttachment, AgentConversationArtifact, AgentConversationMessage, AgentConversationMessagePart, AgentConversationNotice, AgentConversationToolCall, } from "./types.js"; export interface AgentConversationProps { messages: AgentConversationMessage[]; loading?: boolean; error?: string | null; streaming?: boolean; className?: string; timelineClassName?: string; emptyTitle?: string; emptyDescription?: string; composer?: React.ReactNode; } export function AgentConversation({ messages, loading = false, error, className, timelineClassName, emptyTitle = "No messages yet", emptyDescription, composer, }: AgentConversationProps) { return (
{error && (
{error}
)} {loading && messages.length === 0 ? ( } title="Loading session..." /> ) : messages.length === 0 ? ( } title={emptyTitle} description={emptyDescription} /> ) : ( messages.map((message) => ( )) )} ( )} /> {composer}
); } function ConversationEmpty({ icon, title, description, }: { icon: React.ReactNode; title: string; description?: string; }) { return (
{icon}

{title}

{description && {description}}
); } export function AgentConversationMessageView({ message, }: { message: AgentConversationMessage; }) { const parts = message.parts ?? legacyPartsForMessage(message); return (
{message.attachments && message.attachments.length > 0 && (
{message.attachments.map((attachment, i) => ( ))}
)}
{parts.map((part) => ( ))}
); } function legacyPartsForMessage( message: AgentConversationMessage, ): AgentConversationMessagePart[] { return [ ...(message.text ? [ { id: `${message.id}-text`, type: "text" as const, text: message.text, }, ] : []), ...(message.tools ?? []).map((tool) => ({ id: `${message.id}-tool-${tool.id}`, type: "tool" as const, tool, })), ...(message.notices ?? []).map((notice) => ({ id: `${message.id}-notice-${notice.id}`, type: "notice" as const, notice, })), ...(message.artifacts ?? []).map((artifact) => ({ id: `${message.id}-artifact-${artifact.id}`, type: "artifact" as const, artifact, })), ]; } function ConversationMessagePartView({ part, }: { part: AgentConversationMessagePart; }) { return (
{part.type === "text" ? ( ) : part.type === "tool" ? ( ) : part.type === "notice" ? ( ) : ( )}
); } // ─── Shiki syntax highlighter (lazy-loaded) ────────────────────────────────── type ShikiHighlighter = { codeToHtml: ( code: string, options: { lang: string; themes: { light: string; dark: string }; defaultColor?: false | "light" | "dark"; }, ) => string | Promise; getLoadedLanguages: () => string[]; }; let _highlighterLoader: Promise | null = null; function loadConversationHighlighter(): Promise { if (!_highlighterLoader) { _highlighterLoader = (async () => { // Use the JavaScript regex engine instead of Oniguruma WASM (~608 KB saved). // forgiving:true degrades unsupported patterns gracefully instead of throwing. const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] = await Promise.all([ import("shiki/core"), import("shiki/engine/javascript"), ]); return createHighlighterCore({ themes: [ import("shiki/themes/github-light-default.mjs"), import("shiki/themes/github-dark-default.mjs"), ], langs: [ import("shiki/langs/javascript.mjs"), import("shiki/langs/typescript.mjs"), import("shiki/langs/jsx.mjs"), import("shiki/langs/tsx.mjs"), import("shiki/langs/json.mjs"), import("shiki/langs/css.mjs"), import("shiki/langs/html.mjs"), import("shiki/langs/markdown.mjs"), import("shiki/langs/bash.mjs"), import("shiki/langs/shellscript.mjs"), import("shiki/langs/python.mjs"), import("shiki/langs/yaml.mjs"), import("shiki/langs/sql.mjs"), ], engine: createJavaScriptRegexEngine({ forgiving: true }), }) as unknown as Promise; })().catch((err) => { _highlighterLoader = null; throw err; }); } return _highlighterLoader; } function ConversationMarkdown({ text }: { text: string }) { return (
{children}; } return ( openMarkdownLink(event, href)} > {children} ); }, pre(props: React.HTMLAttributes) { const { children, ...rest } = props; if (React.isValidElement(children)) { const childProps = children.props as { className?: string; children?: React.ReactNode; }; const langMatch = (childProps.className ?? "").match( /\blanguage-([\w+-]+)\b/, ); if (langMatch) { const code = extractCodeText(childProps.children).replace( /\n$/, "", ); return ( ); } } return
{children}
; }, }} > {text}
); } function extractCodeText(node: React.ReactNode): string { if (typeof node === "string") return node; if (typeof node === "number") return String(node); if (Array.isArray(node)) return node.map(extractCodeText).join(""); if (React.isValidElement(node)) { return extractCodeText( (node.props as { children?: React.ReactNode }).children, ); } return ""; } function openMarkdownLink( event: React.MouseEvent, href: string | undefined, ) { if (!href) return; let url: URL; try { url = new URL(href, window.location.href); } catch { event.preventDefault(); return; } if (!["http:", "https:", "mailto:", "tel:"].includes(url.protocol)) { event.preventDefault(); return; } event.preventDefault(); window.open(url.href, "_blank", "noopener,noreferrer"); } function parseJsonText(value: string | undefined): unknown { if (!value) return null; try { return JSON.parse(value); } catch { return null; } } function parseJsonObject(value: string | undefined): Record { const parsed = parseJsonText(value); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : {}; } function ConversationToolCall({ tool }: { tool: AgentConversationToolCall }) { const resultJson = parseJsonText(tool.result); const nativeToolContext = { toolName: tool.name, args: tool.args ?? parseJsonObject(tool.input), resultText: tool.result, resultJson: tool.resultJson ?? resultJson, isRunning: tool.state === "running" || tool.state === "activity", chatUI: tool.chatUI, }; const NativeToolRenderer = resolveBuiltinActionChatRenderer(nativeToolContext) ?? resolveToolRenderer(nativeToolContext) ?? resolveBuiltinFallbackToolRenderer(nativeToolContext); if (NativeToolRenderer) { return ( ); } const hasDetails = Boolean(tool.input || tool.result || tool.mcpApp); const icon = tool.state === "running" || tool.state === "activity" ? ( ) : tool.state === "errored" ? ( ) : ( ); const content = ( <> {icon} {humanizeToolName(tool.name)} {tool.summary && ( {tool.summary} )} ); if (!hasDetails) { return
{content}
; } return (
{content}
{tool.mcpApp && } {tool.input && (
            input
            {tool.input}
          
)} {tool.result && (
            result
            {tool.result}
          
)}
); } function ConversationNotice({ notice }: { notice: AgentConversationNotice }) { return (
{notice.title && {notice.title}} {notice.text}
{notice.action}
); } function ConversationArtifact({ artifact, }: { artifact: AgentConversationArtifact; }) { return (
{artifact.path ? ( {artifact.path} ) : ( {artifact.label} )} {artifact.url && ( Open )}
); } function ConversationAttachmentChip({ attachment, }: { attachment: AgentConversationAttachment; }) { if (attachment.dataUrl) { return (
{attachment.name} {attachment.name}
); } return (
{attachment.name} {attachment.size !== undefined && ( {formatBytes(attachment.size)} )}
); } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }