Files
openchamber/packages/ui/src/components/chat/message/ToolOutputDialog.tsx
T

836 lines
49 KiB
TypeScript
Raw Normal View History

2025-12-07 19:32:53 +02:00
import React from 'react';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { RiArrowLeftSLine, RiArrowRightSLine, RiBrainAi3Line, RiCloseLine, RiFileImageLine, RiFileList2Line, RiFilePdfLine, RiFileSearchLine, RiFolder6Line, RiGitBranchLine, RiGlobalLine, RiListCheck3, RiPencilAiLine, RiSearchLine, RiTaskLine, RiTerminalBoxLine, RiToolsLine } from '@remixicon/react';
2025-12-07 19:32:53 +02:00
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { createPortal } from 'react-dom';
2025-12-07 19:32:53 +02:00
import { cn } from '@/lib/utils';
import { SimpleMarkdownRenderer } from '../MarkdownRenderer';
2025-12-07 19:32:53 +02:00
import { toolDisplayStyles } from '@/lib/typography';
import { getLanguageFromExtension } from '@/lib/toolHelpers';
import {
renderTodoOutput,
renderListOutput,
renderGrepOutput,
renderGlobOutput,
renderWebSearchOutput,
formatInputForDisplay,
parseDiffToUnified,
type UnifiedDiffHunk,
type SideBySideDiffHunk,
type SideBySideDiffLine,
} from './toolRenderers';
import type { ToolPopupContent, DiffViewMode } from './types';
import { DiffViewToggle } from './DiffViewToggle';
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 <RiBrainAi3Line className={iconClass} />;
}
if (tool === 'image-preview') {
return <RiFileImageLine className={iconClass} />;
}
if (tool === 'edit' || tool === 'multiedit' || tool === 'apply_patch' || tool === 'str_replace' || tool === 'str_replace_based_edit_tool') {
2025-12-07 19:32:53 +02:00
return <RiPencilAiLine className={iconClass} />;
}
if (tool === 'write' || tool === 'create' || tool === 'file_write') {
return <RiFilePdfLine className={iconClass} />;
}
if (tool === 'read' || tool === 'view' || tool === 'file_read' || tool === 'cat') {
return <RiFilePdfLine className={iconClass} />;
}
if (tool === 'bash' || tool === 'shell' || tool === 'cmd' || tool === 'terminal') {
return <RiTerminalBoxLine className={iconClass} />;
}
if (tool === 'list' || tool === 'ls' || tool === 'dir' || tool === 'list_files') {
return <RiFolder6Line className={iconClass} />;
}
if (tool === 'search' || tool === 'grep' || tool === 'find' || tool === 'ripgrep') {
return <RiSearchLine className={iconClass} />;
}
if (tool === 'glob') {
return <RiFileSearchLine className={iconClass} />;
}
if (tool === 'fetch' || tool === 'curl' || tool === 'wget' || tool === 'webfetch') {
return <RiGlobalLine className={iconClass} />;
}
if (tool === 'web-search' || tool === 'websearch' || tool === 'search_web' || tool === 'google' || tool === 'bing' || tool === 'duckduckgo') {
return <RiSearchLine className={iconClass} />;
}
if (tool === 'todowrite' || tool === 'todoread') {
return <RiListCheck3 className={iconClass} />;
}
if (tool === 'plan_enter') {
return <RiFileList2Line className={iconClass} />;
}
if (tool === 'plan_exit') {
return <RiTaskLine className={iconClass} />;
}
2025-12-07 19:32:53 +02:00
if (tool.startsWith('git')) {
return <RiGitBranchLine className={iconClass} />;
}
return <RiToolsLine className={iconClass} />;
};
const IMAGE_PREVIEW_ANIMATION_MS = 150;
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, setIsRendered] = React.useState(popup.open);
const [isVisible, setIsVisible] = React.useState(popup.open);
const [isTransitioning, setIsTransitioning] = React.useState(false);
const [viewport, setViewport] = React.useState<{ width: number; height: number }>({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});
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) {
setIsRendered(true);
setIsTransitioning(true);
if (typeof window === 'undefined') {
setIsVisible(true);
return;
}
const raf = window.requestAnimationFrame(() => {
setIsVisible(true);
});
const doneId = window.setTimeout(() => {
setIsTransitioning(false);
}, IMAGE_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);
}, IMAGE_PREVIEW_ANIMATION_MS);
return () => {
window.clearTimeout(timeoutId);
};
}, [popup.open]);
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(() => {
if (!popup.open || typeof window === 'undefined') {
return;
}
const onResize = () => {
setViewport({ width: window.innerWidth, height: window.innerHeight });
};
onResize();
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}, [popup.open]);
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 = (
<div className={cn('fixed inset-0 z-50', popup.open ? 'pointer-events-auto' : 'pointer-events-none')}>
<div
aria-hidden="true"
className={cn(
'absolute inset-0 bg-black/25 backdrop-blur-md',
isTransitioning && 'transition-opacity duration-150 ease-out',
isVisible ? 'opacity-100' : 'opacity-0'
)}
onMouseDown={() => onOpenChange(false)}
/>
{hasMultipleImages && (
<>
<button
type="button"
onMouseDown={(event) => event.stopPropagation()}
onClick={showPrevious}
className="absolute left-3 top-1/2 -translate-y-1/2 z-10 h-10 w-10 flex items-center justify-center rounded-full bg-black/25 text-foreground/90 backdrop-blur-sm hover:bg-black/35 focus:outline-none focus:ring-2 focus:ring-primary/60"
aria-label="Previous image"
>
<RiArrowLeftSLine className="h-6 w-6" />
</button>
<button
type="button"
onMouseDown={(event) => event.stopPropagation()}
onClick={showNext}
className="absolute right-3 top-1/2 -translate-y-1/2 z-10 h-10 w-10 flex items-center justify-center rounded-full bg-black/25 text-foreground/90 backdrop-blur-sm hover:bg-black/35 focus:outline-none focus:ring-2 focus:ring-primary/60"
aria-label="Next image"
>
<RiArrowRightSLine className="h-6 w-6" />
</button>
</>
)}
<div
className={cn(
'absolute inset-0 flex items-center justify-center pointer-events-none',
isMobile ? 'p-2.5' : 'p-4'
)}
>
<div
className={cn(
'pointer-events-auto flex flex-col gap-2',
isTransitioning && 'transition-opacity duration-150 ease-out',
isVisible ? 'opacity-100' : 'opacity-0'
)}
style={{ width: `${imageDisplaySize.width}px` }}
>
<div className="flex items-center justify-between gap-2">
<div className="min-w-0 flex-1 text-foreground typography-ui-header font-semibold truncate" title={imageTitle}>
{imageTitle}
</div>
<button
type="button"
className="h-8 w-8 flex items-center justify-center rounded-lg text-muted-foreground/80 hover:text-foreground focus:outline-none focus:ring-2 focus:ring-primary/60"
onClick={() => onOpenChange(false)}
aria-label="Close image preview"
>
<RiCloseLine className="h-4 w-4" />
</button>
</div>
<img
src={currentImage.url}
alt={imageTitle}
className="block object-contain"
style={{ width: `${imageDisplaySize.width}px`, height: `${imageDisplaySize.height}px` }}
loading="lazy"
onLoad={(event) => {
const element = event.currentTarget;
const width = element.naturalWidth;
const height = element.naturalHeight;
if (width > 0 && height > 0) {
setImageNaturalSize((previous) => {
if (previous && previous.width === width && previous.height === height) {
return previous;
}
return { width, height };
});
}
}}
/>
</div>
</div>
</div>
);
return createPortal(content, document.body);
};
2025-12-07 19:32:53 +02:00
const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange, syntaxTheme, isMobile }) => {
const [diffViewMode, setDiffViewMode] = React.useState<DiffViewMode>(isMobile ? 'unified' : 'side-by-side');
if (popup.image) {
return <ImagePreviewDialog popup={popup} onOpenChange={onOpenChange} isMobile={isMobile} />;
}
2025-12-07 19:32:53 +02:00
return (
<Dialog open={popup.open} onOpenChange={onOpenChange}>
<DialogContent
className={cn(
'overflow-hidden flex flex-col min-h-0 pt-3 pb-4 px-4 gap-1',
'[&>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' }}
>
<div className="flex-shrink-0 pb-1">
<div className="flex items-start gap-2 text-foreground typography-ui-header font-semibold">
{popup.metadata?.tool ? getToolIcon(popup.metadata.tool as string) : (
<RiToolsLine className="h-3.5 w-3.5 text-foreground flex-shrink-0" />
)}
<span className="break-words flex-1 leading-tight">{popup.title}</span>
{popup.isDiff && (
<DiffViewToggle
mode={diffViewMode}
onModeChange={setDiffViewMode}
className="mr-8 flex-shrink-0"
/>
)}
</div>
</div>
<div className="flex-1 min-h-0 rounded-xl border border-border/30 bg-muted/10 overflow-hidden">
2025-12-13 16:34:17 +02:00
<div className="tool-output-surface h-full max-h-[75vh] overflow-y-auto px-3 pr-4">
2025-12-07 19:32:53 +02:00
{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' ? (() => {
2025-12-07 19:32:53 +02:00
const meta = popup.metadata!;
const input = meta.input as Record<string, unknown>;
const getInputValue = (key: string): string | null => {
const val = input[key];
return typeof val === 'string' ? val : (typeof val === 'number' ? String(val) : null);
};
return (
<div className="border-b border-border/20 p-4 -mx-3">
<div className="typography-markdown font-medium text-muted-foreground mb-2 px-3">
{meta.tool === 'bash'
? 'Command:'
: meta.tool === 'task'
? 'Task Details:'
: 'Input:'}
</div>
{meta.tool === 'bash' && getInputValue('command') ? (
2025-12-13 16:34:17 +02:00
<div className="tool-input-surface bg-transparent rounded-xl border border-border/20 mx-3">
2025-12-07 19:32:53 +02:00
<SyntaxHighlighter
style={syntaxTheme}
language="bash"
PreTag="div"
customStyle={toolDisplayStyles.getPopupStyles()}
2025-12-13 16:34:17 +02:00
codeTagProps={{ style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } }}
2025-12-07 19:32:53 +02:00
wrapLongLines
>
{getInputValue('command')!}
</SyntaxHighlighter>
</div>
) : meta.tool === 'task' && getInputValue('prompt') ? (
2025-12-13 16:34:17 +02:00
<div
className="tool-input-surface bg-transparent rounded-xl border border-border/20 font-mono whitespace-pre-wrap text-foreground/90 mx-3"
2025-12-07 19:32:53 +02:00
style={toolDisplayStyles.getPopupStyles()}
>
{getInputValue('description') ? `Task: ${getInputValue('description')}\n` : ''}
{getInputValue('subagent_type') ? `Agent Type: ${getInputValue('subagent_type')}\n` : ''}
{`Instructions:\n${getInputValue('prompt')}`}
2025-12-13 16:34:17 +02:00
</div>
2025-12-07 19:32:53 +02:00
) : meta.tool === 'write' && getInputValue('content') ? (
2025-12-13 16:34:17 +02:00
<div className="tool-input-surface bg-transparent rounded-xl border border-border/20 mx-3">
2025-12-07 19:32:53 +02:00
<SyntaxHighlighter
style={syntaxTheme}
language={getLanguageFromExtension(getInputValue('filePath') || getInputValue('file_path') || '') || 'text'}
PreTag="div"
customStyle={toolDisplayStyles.getPopupStyles()}
2025-12-13 16:34:17 +02:00
codeTagProps={{ style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } }}
2025-12-07 19:32:53 +02:00
wrapLongLines
>
{getInputValue('content')!}
</SyntaxHighlighter>
</div>
) : (
2025-12-13 16:34:17 +02:00
<div
className="tool-input-surface bg-transparent rounded-xl border border-border/20 font-mono whitespace-pre-wrap text-foreground/90 mx-3"
2025-12-07 19:32:53 +02:00
style={toolDisplayStyles.getPopupStyles()}
>
{formatInputForDisplay(input, meta.tool as string)}
2025-12-13 16:34:17 +02:00
</div>
2025-12-07 19:32:53 +02:00
)}
</div>
);
})() : null}
{popup.isDiff ? (
diffViewMode === 'unified' ? (
2025-12-13 16:34:17 +02:00
<div className="typography-code">
2025-12-07 19:32:53 +02:00
{parseDiffToUnified(popup.content).map((hunk, hunkIdx) => (
<div key={hunkIdx} className="border-b border-border/20 last:border-b-0">
<div
className={cn('bg-muted/20 px-3 py-2 font-medium text-muted-foreground border-b border-border/10 sticky top-0 z-10 break-words -mx-3', isMobile ? 'typography-micro' : 'typography-markdown')}
>
{`${hunk.file} (line ${hunk.oldStart})`}
</div>
<div>
{hunk.lines.map((line, lineIdx) => (
<div
key={lineIdx}
className={cn(
2025-12-13 16:34:17 +02:00
'typography-code font-mono px-3 py-0.5 flex',
2025-12-07 19:32:53 +02:00
line.type === 'context' && 'bg-transparent',
line.type === 'removed' && 'bg-transparent',
line.type === 'added' && 'bg-transparent'
)}
style={{
lineHeight: '1.1',
...(line.type === 'removed'
? { backgroundColor: 'var(--tools-edit-removed-bg)' }
: line.type === 'added'
? { backgroundColor: 'var(--tools-edit-added-bg)' }
: {}),
}}
>
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
2025-12-07 19:32:53 +02:00
{line.lineNumber || ''}
</span>
<div className="flex-1 min-w-0">
{(() => {
const inputFile = (typeof popup.metadata?.input === 'object' && popup.metadata.input !== null)
? ((popup.metadata.input as Record<string, unknown>).file_path || (popup.metadata.input as Record<string, unknown>).filePath)
: null;
const fileStr = typeof inputFile === 'string' ? inputFile : '';
const hunkFile = (typeof hunk === 'object' && hunk !== null && 'file' in hunk) ? (hunk as UnifiedDiffHunk).file : null;
const hunkFileStr = typeof hunkFile === 'string' ? hunkFile : '';
const finalFile = fileStr || hunkFileStr || '';
return (
<SyntaxHighlighter
style={syntaxTheme}
language={getLanguageFromExtension(finalFile) || 'text'}
PreTag="div"
wrapLines
wrapLongLines
customStyle={{
margin: 0,
padding: 0,
fontSize: 'inherit',
2025-12-13 16:34:17 +02:00
background: 'transparent',
backgroundColor: 'transparent',
2025-12-07 19:32:53 +02:00
borderRadius: 0,
overflow: 'visible',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
overflowWrap: 'anywhere',
}}
codeTagProps={{
style: {
2025-12-13 16:34:17 +02:00
background: 'transparent',
backgroundColor: 'transparent',
2025-12-07 19:32:53 +02:00
},
}}
>
{line.content}
</SyntaxHighlighter>
);
})()}
</div>
</div>
))}
</div>
</div>
))}
</div>
) : popup.diffHunks ? (
2025-12-13 16:34:17 +02:00
<div className="typography-code">
2025-12-07 19:32:53 +02:00
{popup.diffHunks.map((hunk, hunkIdx) => (
<div key={hunkIdx} className="border-b border-border/20 last:border-b-0">
<div
className={cn('bg-muted/20 px-3 py-2 font-medium text-muted-foreground border-b border-border/10 sticky top-0 z-10 break-words -mx-3', isMobile ? 'typography-micro' : 'typography-markdown')}
>
{`${hunk.file} (line ${hunk.oldStart})`}
</div>
<div>
{(hunk as unknown as SideBySideDiffHunk).lines.map((line: SideBySideDiffLine, lineIdx: number) => (
<div key={lineIdx} className="grid grid-cols-2 divide-x divide-border/20">
<div
className={cn(
2025-12-13 16:34:17 +02:00
'typography-code font-mono px-3 py-0.5 overflow-hidden',
2025-12-07 19:32:53 +02:00
line.leftLine.type === 'context' && 'bg-transparent',
line.leftLine.type === 'empty' && 'bg-transparent'
)}
style={{
lineHeight: '1.1',
...(line.leftLine.type === 'removed' ? { backgroundColor: 'var(--tools-edit-removed-bg)' } : {}),
}}
>
<div className="flex">
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
2025-12-07 19:32:53 +02:00
{line.leftLine.lineNumber || ''}
</span>
<div className="flex-1 min-w-0">
{line.leftLine.content && (
<SyntaxHighlighter
style={syntaxTheme}
language={(() => {
const input = popup.metadata?.input;
const inputObj = typeof input === 'object' && input !== null ? (input as Record<string, unknown>) : {};
const filePath = inputObj.file_path || inputObj.filePath;
const hunkFile = (hunk as unknown as UnifiedDiffHunk).file;
return getLanguageFromExtension(typeof filePath === 'string' ? filePath : hunkFile) || 'text';
})()}
PreTag="div"
wrapLines
wrapLongLines
customStyle={{
margin: 0,
padding: 0,
fontSize: 'inherit',
2025-12-13 16:34:17 +02:00
background: 'transparent',
backgroundColor: 'transparent',
2025-12-07 19:32:53 +02:00
borderRadius: 0,
overflow: 'visible',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
overflowWrap: 'anywhere',
}}
codeTagProps={{
style: {
2025-12-13 16:34:17 +02:00
background: 'transparent',
backgroundColor: 'transparent',
2025-12-07 19:32:53 +02:00
},
}}
>
{line.leftLine.content}
</SyntaxHighlighter>
)}
</div>
</div>
</div>
<div
className={cn(
2025-12-13 16:34:17 +02:00
'typography-code font-mono px-3 py-0.5 overflow-hidden',
2025-12-07 19:32:53 +02:00
line.rightLine.type === 'context' && 'bg-transparent',
line.rightLine.type === 'empty' && 'bg-transparent'
)}
style={{
lineHeight: '1.1',
...(line.rightLine.type === 'added' ? { backgroundColor: 'var(--tools-edit-added-bg)' } : {}),
}}
>
<div className="flex">
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
2025-12-07 19:32:53 +02:00
{line.rightLine.lineNumber || ''}
</span>
<div className="flex-1 min-w-0">
{line.rightLine.content && (
<SyntaxHighlighter
style={syntaxTheme}
language={(() => {
const input = popup.metadata?.input;
const inputObj = typeof input === 'object' && input !== null ? (input as Record<string, unknown>) : {};
const filePath = inputObj.file_path || inputObj.filePath;
const hunkFile = (hunk as unknown as UnifiedDiffHunk).file;
return getLanguageFromExtension(typeof filePath === 'string' ? filePath : hunkFile) || 'text';
})()}
PreTag="div"
wrapLines
wrapLongLines
customStyle={{
margin: 0,
padding: 0,
fontSize: 'inherit',
2025-12-13 16:34:17 +02:00
background: 'transparent',
backgroundColor: 'transparent',
2025-12-07 19:32:53 +02:00
borderRadius: 0,
overflow: 'visible',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
overflowWrap: 'anywhere',
}}
codeTagProps={{
style: {
2025-12-13 16:34:17 +02:00
background: 'transparent',
backgroundColor: 'transparent',
2025-12-07 19:32:53 +02:00
},
}}
>
{line.rightLine.content}
</SyntaxHighlighter>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
) : null
) : popup.content ? (
<div className="p-4">
{(() => {
const tool = popup.metadata?.tool;
if (tool === 'todowrite' || tool === 'todoread') {
return (
renderTodoOutput(popup.content) || (
<SyntaxHighlighter
style={syntaxTheme}
language="json"
PreTag="div"
wrapLongLines
customStyle={toolDisplayStyles.getPopupContainerStyles()}
2025-12-13 16:34:17 +02:00
codeTagProps={{ style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } }}
2025-12-07 19:32:53 +02:00
>
{popup.content}
</SyntaxHighlighter>
)
);
}
if (tool === 'list') {
return (
renderListOutput(popup.content) || (
<pre className="typography-markdown bg-muted/30 p-2 rounded-xl border border-border/20 font-mono whitespace-pre-wrap">
{popup.content}
</pre>
)
2025-12-13 16:34:17 +02:00
);
2025-12-07 19:32:53 +02:00
}
if (tool === 'grep') {
return (
renderGrepOutput(popup.content, isMobile) || (
2025-12-13 16:34:17 +02:00
<pre className="typography-code bg-muted/30 p-2 rounded-xl border border-border/20 font-mono whitespace-pre-wrap">
2025-12-07 19:32:53 +02:00
{popup.content}
</pre>
)
);
}
if (tool === 'glob') {
return (
renderGlobOutput(popup.content, isMobile) || (
2025-12-13 16:34:17 +02:00
<pre className="typography-code bg-muted/30 p-2 rounded-xl border border-border/20 font-mono whitespace-pre-wrap">
2025-12-07 19:32:53 +02:00
{popup.content}
</pre>
)
);
}
if (tool === 'task' || tool === 'reasoning') {
return (
<div className={tool === 'reasoning' ? "text-muted-foreground/70" : ""}>
<SimpleMarkdownRenderer content={popup.content} variant="tool" />
2025-12-07 19:32:53 +02:00
</div>
);
}
if (tool === 'web-search' || tool === 'websearch' || tool === 'search_web') {
return (
renderWebSearchOutput(popup.content, syntaxTheme) || (
<SyntaxHighlighter
style={syntaxTheme}
language="text"
PreTag="div"
wrapLongLines
customStyle={toolDisplayStyles.getPopupContainerStyles()}
2025-12-13 16:34:17 +02:00
codeTagProps={{ style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } }}
2025-12-07 19:32:53 +02:00
>
{popup.content}
</SyntaxHighlighter>
)
);
}
if (tool === 'read') {
const lines = popup.content.split('\n');
const inputMeta = popup.metadata?.input;
const inputObj = typeof inputMeta === 'object' && inputMeta !== null ? (inputMeta as Record<string, unknown>) : {};
const offset = typeof inputObj.offset === 'number' ? inputObj.offset : 0;
const limit = typeof inputObj.limit === 'number' ? inputObj.limit : undefined;
const isInfoMessage = (line: string) => line.trim().startsWith('(');
return (
<div>
{lines.map((line: string, idx: number) => {
const isInfo = isInfoMessage(line);
const lineNumber = offset + idx + 1;
const shouldShowLineNumber = !isInfo && (limit === undefined || idx < limit);
return (
<div key={idx} className={`typography-code font-mono flex ${isInfo ? 'text-muted-foreground/70 italic' : ''}`}>
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
{shouldShowLineNumber ? lineNumber : ''}
</span>
<div className="flex-1 min-w-0">
2025-12-07 19:32:53 +02:00
{isInfo ? (
<div className="whitespace-pre-wrap break-words">{line}</div>
) : (
<SyntaxHighlighter
style={syntaxTheme}
language={popup.language || 'text'}
PreTag="div"
wrapLines
wrapLongLines
customStyle={{
margin: 0,
padding: 0,
fontSize: 'inherit',
2025-12-13 16:34:17 +02:00
background: 'transparent',
backgroundColor: 'transparent',
2025-12-07 19:32:53 +02:00
borderRadius: 0,
overflow: 'visible',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
overflowWrap: 'anywhere',
}}
codeTagProps={{
style: {
2025-12-13 16:34:17 +02:00
background: 'transparent',
backgroundColor: 'transparent',
fontSize: 'inherit',
2025-12-07 19:32:53 +02:00
},
}}
>
{line}
</SyntaxHighlighter>
)}
</div>
</div>
);
})}
</div>
);
}
return (
<SyntaxHighlighter
style={syntaxTheme}
language={popup.language || 'text'}
PreTag="div"
wrapLongLines
customStyle={toolDisplayStyles.getPopupContainerStyles()}
2025-12-13 16:34:17 +02:00
codeTagProps={{ style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } }}
2025-12-07 19:32:53 +02:00
>
{popup.content}
</SyntaxHighlighter>
);
})()}
</div>
) : (
<div className="p-8 text-muted-foreground typography-ui-header">
<div className="mb-2">Command completed successfully</div>
2025-12-13 16:34:17 +02:00
<div className="typography-meta">No output was produced</div>
2025-12-07 19:32:53 +02:00
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
};
export default ToolOutputDialog;