/** * ShellRenderer Component * * Renders shell command execution results with exit code badge, * duration, and stdout/stderr output. */ import { useState } from 'react'; import { Terminal, ChevronDown, ChevronRight, Copy, Check, Clock } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { ShellDisplayData } from '@dexto/core'; interface ShellRendererProps { /** Shell display data from tool result */ data: ShellDisplayData; /** Maximum lines before truncation (default: 10) */ maxLines?: number; /** Whether to start expanded (default: based on exit code) */ defaultExpanded?: boolean; } /** * Format duration in human-readable format. */ function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; } /** * Renders shell command result with collapsible output. */ export function ShellRenderer({ data, maxLines = 10, defaultExpanded }: ShellRendererProps) { const { command, exitCode, duration, stdout, stderr, isBackground } = data; // Expand by default if there was an error const [expanded, setExpanded] = useState(defaultExpanded ?? exitCode !== 0); const [showAll, setShowAll] = useState(false); const [copied, setCopied] = useState(false); const output = stdout || stderr || ''; const lines = output.split('\n').filter((line) => line.length > 0); const shouldTruncate = lines.length > maxLines && !showAll; const displayLines = shouldTruncate ? lines.slice(0, maxLines) : lines; const isSuccess = exitCode === 0; const handleCopy = async () => { try { await navigator.clipboard.writeText(output); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // Clipboard API failed - non-secure context or permission denied console.warn('Failed to copy to clipboard'); } }; return (
{/* Header with command and metadata */}
{/* Command (truncated) */}
{command.length > 60 ? `${command.substring(0, 60)}...` : command}
{/* Badges */}
{/* Exit code badge */} {isSuccess ? 'exit 0' : `exit ${exitCode}`} {/* Duration */} {formatDuration(duration)} {/* Background indicator */} {isBackground && ( bg )}
{/* Output section */} {lines.length > 0 && (
{!expanded ? ( ) : (
                                    {displayLines.join('\n')}
                                
{shouldTruncate && ( )}
)}
)} {/* No output indicator */} {lines.length === 0 && (
(no output)
)}
); }