import React from 'react'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { RiArrowLeftSLine, RiArrowRightSLine, RiBrainAi3Line, RiCloseLine, RiFileImageLine, RiFileList2Line, RiFilePdfLine, RiFileSearchLine, RiFolder6Line, RiGitBranchLine, RiGlobalLine, RiListCheck3, RiLoader4Line, RiPencilAiLine, RiSearchLine, RiTaskLine, RiTerminalBoxLine, RiToolsLine } from '@remixicon/react'; import { File as PierreFile, PatchDiff } from '@pierre/diffs/react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { createPortal } from 'react-dom'; import { cn } from '@/lib/utils'; import { SimpleMarkdownRenderer } from '../MarkdownRenderer'; import { toolDisplayStyles } from '@/lib/typography'; import { getLanguageFromExtension } from '@/lib/toolHelpers'; import { useOptionalThemeSystem } from '@/contexts/useThemeSystem'; import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry'; import { getDefaultTheme } from '@/lib/theme/themes'; import { renderTodoOutput, renderListOutput, renderGrepOutput, renderGlobOutput, renderWebSearchOutput, formatInputForDisplay, parseReadToolOutput, tryParseJsonOutput, } from './toolRenderers'; import type { ToolPopupContent, DiffViewMode } from './types'; import { DiffViewToggle } from './DiffViewToggle'; import { VirtualizedCodeBlock, type CodeLine } from './parts/VirtualizedCodeBlock'; import { JsonTreeView } from '@/components/ui/JsonTreeView'; interface ToolOutputDialogProps { popup: ToolPopupContent; onOpenChange: (open: boolean) => void; syntaxTheme: { [key: string]: React.CSSProperties }; isMobile: boolean; } const getToolIcon = (toolName: string) => { const iconClass = 'h-3.5 w-3.5 flex-shrink-0'; const tool = toolName.toLowerCase(); if (tool === 'reasoning') { return ; } if (tool === 'image-preview') { return ; } if (tool === 'mermaid-preview') { return ; } if (tool === 'edit' || tool === 'multiedit' || tool === 'apply_patch' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') { return ; } if (tool === 'write' || tool === 'create' || tool === 'file_write') { return ; } if (tool === 'read' || tool === 'view' || tool === 'file_read' || tool === 'cat') { return ; } if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal') { return ; } if (tool === 'list' || tool === 'ls' || tool === 'dir' || tool === 'list_files') { return ; } if (tool === 'search' || tool === 'grep' || tool === 'find' || tool === 'ripgrep') { return ; } if (tool === 'glob') { return ; } if (tool === 'fetch' || tool === 'curl' || tool === 'wget' || tool === 'webfetch') { return ; } if (tool === 'web-search' || tool === 'websearch' || tool === 'search_web' || tool === 'google' || tool === 'bing' || tool === 'duckduckgo') { return ; } if (tool === 'todowrite' || tool === 'todoread') { return ; } if (tool === 'plan_enter') { return ; } if (tool === 'plan_exit') { return ; } if (tool.startsWith('git')) { return ; } return ; }; const PREVIEW_ANIMATION_MS = 150; const MERMAID_DIALOG_HEADER_HEIGHT = 40; const MERMAID_ASPECT_RETRY_DELAY_MS = 120; const MERMAID_ASPECT_MAX_RETRIES = 3; type PierreThemeConfig = { theme: { light: string; dark: string }; themeType: 'light' | 'dark'; }; const TOOL_DIFF_UNSAFE_CSS = ` [data-diff-header], [data-diff] { [data-separator] { height: 24px !important; } } `; const TOOL_DIFF_METRICS = { hunkLineCount: 50, lineHeight: 24, diffHeaderHeight: 44, hunkSeparatorHeight: 24, fileGap: 0, }; const usePierreThemeConfig = (): PierreThemeConfig => { const themeSystem = useOptionalThemeSystem(); const fallbackLightTheme = React.useMemo(() => getDefaultTheme(false), []); const fallbackDarkTheme = React.useMemo(() => getDefaultTheme(true), []); const availableThemes = React.useMemo( () => themeSystem?.availableThemes ?? [fallbackLightTheme, fallbackDarkTheme], [fallbackDarkTheme, fallbackLightTheme, themeSystem?.availableThemes], ); const lightThemeId = themeSystem?.lightThemeId ?? fallbackLightTheme.metadata.id; const darkThemeId = themeSystem?.darkThemeId ?? fallbackDarkTheme.metadata.id; const lightTheme = React.useMemo( () => availableThemes.find((theme) => theme.metadata.id === lightThemeId) ?? fallbackLightTheme, [availableThemes, fallbackLightTheme, lightThemeId], ); const darkTheme = React.useMemo( () => availableThemes.find((theme) => theme.metadata.id === darkThemeId) ?? fallbackDarkTheme, [availableThemes, darkThemeId, fallbackDarkTheme], ); React.useEffect(() => { ensurePierreThemeRegistered(lightTheme); ensurePierreThemeRegistered(darkTheme); }, [darkTheme, lightTheme]); const currentVariant = themeSystem?.currentTheme.metadata.variant ?? 'light'; return { theme: { light: lightTheme.metadata.id, dark: darkTheme.metadata.id }, themeType: currentVariant === 'dark' ? 'dark' : 'light', }; }; type ViewportSize = { width: number; height: number }; const getWindowViewport = (): ViewportSize => ({ width: typeof window !== 'undefined' ? window.innerWidth : 0, height: typeof window !== 'undefined' ? window.innerHeight : 0, }); const PREVIEW_VIEWPORT_LIMITS = { mobile: { widthRatio: 0.94, heightRatio: 0.86, padding: 10 }, desktop: { widthRatio: 0.8, heightRatio: 0.8, padding: 16 }, } as const; const getPreviewViewportBounds = (viewport: { width: number; height: number }, isMobile: boolean) => { const limits = isMobile ? PREVIEW_VIEWPORT_LIMITS.mobile : PREVIEW_VIEWPORT_LIMITS.desktop; const paddedWidth = Math.max(160, viewport.width - limits.padding * 2); const paddedHeight = Math.max(160, viewport.height - limits.padding * 2); return { maxWidth: Math.max(160, Math.min(paddedWidth, viewport.width * limits.widthRatio)), maxHeight: Math.max(160, Math.min(paddedHeight, viewport.height * limits.heightRatio)), }; }; const getSvgAspectRatio = (svg: SVGElement): number | null => { try { const groups = Array.from(svg.querySelectorAll('g')); let bestArea = 0; let bestRatio: number | null = null; for (const group of groups) { if (!(group instanceof SVGGraphicsElement)) { continue; } const box = group.getBBox(); if (!(box.width > 0 && box.height > 0)) { continue; } const area = box.width * box.height; if (area > bestArea) { bestArea = area; bestRatio = box.width / box.height; } } if (bestRatio && Number.isFinite(bestRatio) && bestRatio > 0) { return bestRatio; } } catch { // Ignore getBBox failures and fall back to SVG attrs/viewBox. } const viewBox = svg.getAttribute('viewBox'); if (viewBox) { const parts = viewBox.trim().split(/\s+/).map(Number); if (parts.length === 4) { const width = parts[2]; const height = parts[3]; if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { return width / height; } } } const attrWidth = Number(svg.getAttribute('width')); const attrHeight = Number(svg.getAttribute('height')); if (Number.isFinite(attrWidth) && Number.isFinite(attrHeight) && attrWidth > 0 && attrHeight > 0) { return attrWidth / attrHeight; } const rect = svg.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { return rect.width / rect.height; } return null; }; const usePreviewOverlayState = (open: boolean) => { const [isRendered, setIsRendered] = React.useState(open); const [isVisible, setIsVisible] = React.useState(open); const [isTransitioning, setIsTransitioning] = React.useState(false); React.useEffect(() => { if (open) { setIsRendered(true); setIsTransitioning(true); if (typeof window === 'undefined') { setIsVisible(true); return; } const raf = window.requestAnimationFrame(() => { setIsVisible(true); }); const doneId = window.setTimeout(() => { setIsTransitioning(false); }, PREVIEW_ANIMATION_MS); return () => { window.cancelAnimationFrame(raf); window.clearTimeout(doneId); }; } setIsVisible(false); setIsTransitioning(true); if (typeof window === 'undefined') { setIsRendered(false); return; } const timeoutId = window.setTimeout(() => { setIsRendered(false); setIsTransitioning(false); }, PREVIEW_ANIMATION_MS); return () => { window.clearTimeout(timeoutId); }; }, [open]); return { isRendered, isVisible, isTransitioning }; }; const usePreviewViewport = (open: boolean) => { const [viewport, setViewport] = React.useState(getWindowViewport); React.useEffect(() => { if (!open || typeof window === 'undefined') { return; } const onResize = () => { setViewport(getWindowViewport()); }; onResize(); window.addEventListener('resize', onResize); return () => { window.removeEventListener('resize', onResize); }; }, [open]); return viewport; }; const ImagePreviewDialog: React.FC<{ popup: ToolPopupContent; onOpenChange: (open: boolean) => void; isMobile: boolean; }> = ({ popup, onOpenChange, isMobile }) => { const gallery = React.useMemo(() => { const baseImage = popup.image; if (!baseImage) return [] as Array<{ url: string; mimeType?: string; filename?: string; size?: number }>; const fromPopup = Array.isArray(baseImage.gallery) ? baseImage.gallery.filter((item): item is { url: string; mimeType?: string; filename?: string; size?: number } => Boolean(item?.url)) : []; if (fromPopup.length > 0) { return fromPopup; } return [{ url: baseImage.url, mimeType: baseImage.mimeType, filename: baseImage.filename, size: baseImage.size, }]; }, [popup.image]); const [currentIndex, setCurrentIndex] = React.useState(0); const [imageNaturalSize, setImageNaturalSize] = React.useState<{ width: number; height: number } | null>(null); const { isRendered, isVisible, isTransitioning } = usePreviewOverlayState(popup.open); const viewport = usePreviewViewport(popup.open); React.useEffect(() => { if (!popup.open || gallery.length === 0) { return; } const requestedIndex = typeof popup.image?.index === 'number' ? popup.image.index : -1; if (requestedIndex >= 0 && requestedIndex < gallery.length) { setCurrentIndex(requestedIndex); return; } const matchingIndex = popup.image?.url ? gallery.findIndex((item) => item.url === popup.image?.url) : -1; setCurrentIndex(matchingIndex >= 0 ? matchingIndex : 0); }, [gallery, popup.image?.index, popup.image?.url, popup.open]); const currentImage = gallery[currentIndex] ?? gallery[0] ?? popup.image; const imageTitle = currentImage?.filename || popup.title || 'Image preview'; const hasMultipleImages = gallery.length > 1; const showPrevious = React.useCallback(() => { if (gallery.length <= 1) return; setCurrentIndex((prev) => (prev - 1 + gallery.length) % gallery.length); }, [gallery.length]); const showNext = React.useCallback(() => { if (gallery.length <= 1) return; setCurrentIndex((prev) => (prev + 1) % gallery.length); }, [gallery.length]); React.useEffect(() => { if (!popup.open) { return; } const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { onOpenChange(false); return; } if (event.key === 'ArrowLeft' && hasMultipleImages) { event.preventDefault(); showPrevious(); return; } if (event.key === 'ArrowRight' && hasMultipleImages) { event.preventDefault(); showNext(); } }; window.addEventListener('keydown', onKeyDown); return () => { window.removeEventListener('keydown', onKeyDown); }; }, [hasMultipleImages, onOpenChange, popup.open, showNext, showPrevious]); React.useEffect(() => { setImageNaturalSize(null); }, [currentImage?.url]); const imageDisplaySize = React.useMemo(() => { const maxWidth = Math.max(160, viewport.width * (isMobile ? 0.86 : 0.75)); const maxHeight = Math.max(160, viewport.height * (isMobile ? 0.72 : 0.75)); if (!imageNaturalSize) { return { width: Math.round(maxWidth), height: Math.round(maxHeight), }; } const widthScale = maxWidth / imageNaturalSize.width; const heightScale = maxHeight / imageNaturalSize.height; const scale = Math.min(widthScale, heightScale); return { width: Math.max(1, Math.round(imageNaturalSize.width * scale)), height: Math.max(1, Math.round(imageNaturalSize.height * scale)), }; }, [imageNaturalSize, isMobile, viewport.height, viewport.width]); if (!isRendered || !currentImage || typeof document === 'undefined') { return null; } const content = (
); return createPortal(content, document.body); }; // ── PERF-007: Virtualised sub-components for dialog ────────────────── const DialogUnifiedDiff: React.FC<{ popup: ToolPopupContent; diffViewMode: DiffViewMode; pierreThemeConfig: PierreThemeConfig; }> = React.memo(({ popup, diffViewMode, pierreThemeConfig }) => { const patchContent = popup.content || ''; return (
); }); DialogUnifiedDiff.displayName = 'DialogUnifiedDiff'; const DialogReadContent: React.FC<{ popup: ToolPopupContent; syntaxTheme: Record; pierreThemeConfig: PierreThemeConfig; }> = React.memo(({ popup, syntaxTheme, pierreThemeConfig }) => { const parsedReadOutput = React.useMemo(() => parseReadToolOutput(popup.content), [popup.content]); const inputMeta = popup.metadata?.input; const inputObj = typeof inputMeta === 'object' && inputMeta !== null ? (inputMeta as Record) : {}; const offset = typeof inputObj.offset === 'number' ? inputObj.offset : 0; const filePath = typeof inputObj.file_path === 'string' ? inputObj.file_path : typeof inputObj.filePath === 'string' ? inputObj.filePath : typeof inputObj.path === 'string' ? inputObj.path : 'read-output'; const fileContents = React.useMemo(() => parsedReadOutput.lines.map((line) => line.text).join('\n'), [parsedReadOutput]); const detectedLanguage = React.useMemo( () => popup.language || getLanguageFromExtension(filePath) || 'text', [filePath, popup.language], ); const codeLines: CodeLine[] = React.useMemo(() => { const hasExplicitLineNumbers = parsedReadOutput.lines.some((line) => line.lineNumber !== null); const result: CodeLine[] = []; let nextLineNumber = offset; for (const line of parsedReadOutput.lines) { if (line.lineNumber !== null) { nextLineNumber = line.lineNumber; } const shouldAssignFallback = parsedReadOutput.type === 'file' && !hasExplicitLineNumbers && line.lineNumber === null && !line.isInfo; const effectiveLineNumber = line.lineNumber ?? (shouldAssignFallback ? (nextLineNumber + 1) : null); if (typeof effectiveLineNumber === 'number') { nextLineNumber = effectiveLineNumber; } result.push({ text: line.text, lineNumber: effectiveLineNumber, isInfo: line.isInfo, }); } return result; }, [offset, parsedReadOutput]); if (parsedReadOutput.type === 'file') { return ( ); } return ( ); }); DialogReadContent.displayName = 'DialogReadContent'; const MermaidPreviewDialog: React.FC<{ popup: ToolPopupContent; onOpenChange: (open: boolean) => void; isMobile: boolean; }> = ({ popup, onOpenChange, isMobile }) => { const [source, setSource] = React.useState(popup.mermaid?.source || ''); const [status, setStatus] = React.useState<'idle' | 'loading' | 'ready' | 'error'>(popup.mermaid?.source ? 'ready' : 'idle'); const [errorMessage, setErrorMessage] = React.useState(''); const { isRendered, isVisible, isTransitioning } = usePreviewOverlayState(popup.open); const [diagramAspectRatio, setDiagramAspectRatio] = React.useState(null); const viewport = usePreviewViewport(popup.open); const requestIdRef = React.useRef(0); const mermaidPreviewRef = React.useRef(null); const normalizeFilePath = React.useCallback((rawPath: string): string | null => { const input = rawPath.trim(); if (!input.toLowerCase().startsWith('file://')) { return null; } const isSafeLocalPath = (path: string): boolean => { if (!path || /[\0\r\n]/.test(path)) { return false; } const normalized = path.replace(/\\/g, '/'); const segments = normalized.split('/').filter(Boolean); if (segments.includes('..')) { return false; } if (normalized.startsWith('/')) { return true; } return /^[A-Za-z]:\//.test(normalized); }; const decodeLoose = (value: string): string => { return value.replace(/%([0-9A-Fa-f]{2})/g, (_match, hex: string) => { const codePoint = Number.parseInt(hex, 16); return Number.isFinite(codePoint) ? String.fromCharCode(codePoint) : `%${hex}`; }); }; const canParse = typeof URL.canParse === 'function' ? URL.canParse(input) : false; if (canParse) { let pathname = decodeLoose(new URL(input).pathname || ''); if (/^\/[A-Za-z]:\//.test(pathname)) { pathname = pathname.slice(1); } return isSafeLocalPath(pathname) ? pathname : null; } const stripped = input.replace(/^file:\/\//i, ''); const decoded = decodeLoose(stripped); return isSafeLocalPath(decoded) ? decoded : (isSafeLocalPath(stripped) ? stripped : null); }, []); const decodeDataUrl = React.useCallback((value: string): string => { const commaIndex = value.indexOf(','); if (commaIndex < 0) { throw new Error('Malformed data URL'); } const metadata = value.slice(0, commaIndex).toLowerCase(); const payload = value.slice(commaIndex + 1); if (metadata.includes(';base64')) { return atob(payload); } return decodeURIComponent(payload); }, []); const loadMermaidSource = React.useCallback(async () => { const target = popup.mermaid; if (!target?.url) { setStatus('error'); setErrorMessage('Missing Mermaid source URL.'); return; } if (target.source) { setSource(target.source); setStatus('ready'); setErrorMessage(''); return; } const requestId = requestIdRef.current + 1; requestIdRef.current = requestId; setStatus('loading'); setErrorMessage(''); let sourcePromise: Promise; if (target.url.startsWith('data:')) { sourcePromise = Promise.resolve(decodeDataUrl(target.url)); } else if (target.url.toLowerCase().startsWith('file://')) { const normalizedPath = normalizeFilePath(target.url); if (!normalizedPath) { sourcePromise = Promise.reject(new Error('Invalid local file path for Mermaid preview.')); } else { sourcePromise = fetch(`/api/fs/raw?path=${encodeURIComponent(normalizedPath)}`) .then((response) => { if (!response.ok) { return Promise.reject(new Error(`Failed to read diagram file (${response.status})`)); } return response.text(); }); } } else { const canParse = typeof URL.canParse === 'function' ? URL.canParse(target.url, window.location.origin) : false; const resolvedUrl = canParse ? new URL(target.url, window.location.origin) : null; if (!resolvedUrl || (resolvedUrl.protocol !== 'http:' && resolvedUrl.protocol !== 'https:')) { sourcePromise = Promise.reject(new Error('Unsupported Mermaid URL protocol.')); } else { sourcePromise = fetch(resolvedUrl.toString()) .then((response) => { if (!response.ok) { return Promise.reject(new Error(`Failed to load diagram (${response.status})`)); } return response.text(); }); } } await sourcePromise .then((resolvedSource) => { if (requestIdRef.current !== requestId) { return; } setSource(resolvedSource); setStatus('ready'); }) .catch((error) => { if (requestIdRef.current !== requestId) { return; } setStatus('error'); setErrorMessage(error instanceof Error ? error.message : 'Unable to load Mermaid diagram.'); }); }, [decodeDataUrl, normalizeFilePath, popup.mermaid]); React.useEffect(() => { if (!popup.open || !popup.mermaid) { return; } void loadMermaidSource(); }, [loadMermaidSource, popup.mermaid, popup.open]); React.useEffect(() => { if (!popup.open) { return; } const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { onOpenChange(false); } }; window.addEventListener('keydown', onKeyDown); return () => { window.removeEventListener('keydown', onKeyDown); }; }, [onOpenChange, popup.open]); React.useEffect(() => { if (!popup.open || status !== 'ready') { setDiagramAspectRatio(null); return; } const measureAspectRatio = () => { const svg = mermaidPreviewRef.current?.querySelector('svg'); if (!svg) { return false; } const aspectRatio = getSvgAspectRatio(svg as SVGElement); if (!aspectRatio || !Number.isFinite(aspectRatio) || aspectRatio <= 0) { return false; } setDiagramAspectRatio((previous) => { if (previous && Math.abs(previous - aspectRatio) < 0.001) { return previous; } return aspectRatio; }); return true; }; let rafId = window.requestAnimationFrame(() => { if (!measureAspectRatio()) { rafId = window.requestAnimationFrame(() => { measureAspectRatio(); }); } }); let retryCount = 0; let timeoutId: number | undefined; const scheduleRetry = () => { if (retryCount >= MERMAID_ASPECT_MAX_RETRIES) { return; } timeoutId = window.setTimeout(() => { retryCount += 1; if (!measureAspectRatio()) { scheduleRetry(); } }, MERMAID_ASPECT_RETRY_DELAY_MS); }; scheduleRetry(); const observer = new MutationObserver(() => { measureAspectRatio(); }); if (mermaidPreviewRef.current) { observer.observe(mermaidPreviewRef.current, { childList: true, subtree: true, attributes: true }); } return () => { window.cancelAnimationFrame(rafId); if (typeof timeoutId === 'number') { window.clearTimeout(timeoutId); } observer.disconnect(); }; }, [popup.open, source, status]); const mermaidMarkdown = `\`\`\`mermaid\n${source}\n\`\`\``; const dialogSize = React.useMemo(() => { const { maxWidth, maxHeight } = getPreviewViewportBounds(viewport, isMobile); const availableDiagramHeight = Math.max(160, maxHeight - MERMAID_DIALOG_HEADER_HEIGHT); if (diagramAspectRatio && diagramAspectRatio < 1) { const squareSide = Math.min(maxWidth, availableDiagramHeight); return { width: Math.round(squareSide), height: Math.round(squareSide) }; } return { width: Math.round(maxWidth), height: Math.round(availableDiagramHeight) }; }, [diagramAspectRatio, isMobile, viewport]); if (!isRendered || typeof document === 'undefined') { return null; } const content = (
); return createPortal(content, document.body); }; const ToolOutputDialog: React.FC = ({ popup, onOpenChange, syntaxTheme, isMobile }) => { const [diffViewMode, setDiffViewMode] = React.useState('unified'); const pierreThemeConfig = usePierreThemeConfig(); React.useEffect(() => { if (!popup.open) return; setDiffViewMode('unified'); }, [popup.open, popup.title]); if (popup.image) { return ; } if (popup.mermaid) { return ; } return ( button]:top-1.5', isMobile ? 'w-[95vw] max-w-[95vw]' : 'max-w-5xl', isMobile ? '[&>button]:right-1' : '[&>button]:top-2.5 [&>button]:right-4' )} style={{ maxHeight: '90vh' }} >
{popup.metadata?.tool ? getToolIcon(popup.metadata.tool as string) : ( )} {popup.title} {popup.isDiff && ( )}
{popup.metadata?.input && typeof popup.metadata.input === 'object' && Object.keys(popup.metadata.input).length > 0 && popup.metadata?.tool !== 'todowrite' && popup.metadata?.tool !== 'todoread' && popup.metadata?.tool !== 'apply_patch' ? (() => { const meta = popup.metadata!; const input = meta.input as Record; const getInputValue = (key: string): string | null => { const val = input[key]; return typeof val === 'string' ? val : (typeof val === 'number' ? String(val) : null); }; return (
{meta.tool === 'bash' ? 'Command:' : meta.tool === 'task' ? 'Task Details:' : 'Input:'}
{meta.tool === 'bash' && getInputValue('command') ? (
{getInputValue('command')!}
) : meta.tool === 'task' && getInputValue('prompt') ? (
{getInputValue('description') ? `Task: ${getInputValue('description')}\n` : ''} {getInputValue('subagent_type') ? `Agent Type: ${getInputValue('subagent_type')}\n` : ''} {`Instructions:\n${getInputValue('prompt')}`}
) : meta.tool === 'write' && getInputValue('content') ? (
) : (
{formatInputForDisplay(input, meta.tool as string)}
)}
); })() : null} {popup.isDiff ? ( ) : popup.content ? (
{(() => { const tool = popup.metadata?.tool; if (tool === 'todowrite' || tool === 'todoread') { return ( renderTodoOutput(popup.content) || ( {popup.content} ) ); } if (tool === 'list') { return ( renderListOutput(popup.content) || (
                                                {popup.content}
                                            
) ); } if (tool === 'grep') { return ( renderGrepOutput(popup.content, isMobile) || (
                                                {popup.content}
                                            
) ); } if (tool === 'glob') { return ( renderGlobOutput(popup.content, isMobile) || (
                                                {popup.content}
                                            
) ); } if (tool === 'task' || tool === 'reasoning') { return (
); } if (tool === 'web-search' || tool === 'websearch' || tool === 'search_web') { return ( renderWebSearchOutput(popup.content, syntaxTheme) || ( {popup.content} ) ); } if (tool === 'read') { return ; } // JSON tree viewer for generic JSON outputs const jsonResult = popup.content ? tryParseJsonOutput(popup.content) : { data: null, isJson: false }; if (jsonResult.isJson) { return ( ); } return ( {popup.content} ); })()}
) : (
Command completed successfully
No output was produced
)}
); }; export default ToolOutputDialog;