/** * BashCell — renders a bash tool call with a collapsible terminal-style output. * * Shows: * - Header: `$ ` + exit-code badge + duration * - Body: monospace streaming output, stick-to-bottom while running, * capped at MAX_VISIBLE_LINES by default (expandable) */ import { IconChevronDown, IconCircleCheck, IconCircleX, IconClock, IconLoader2, IconTerminal2, } from "@tabler/icons-react"; import { useEffect, useRef, useState } from "react"; import { cn } from "../utils.js"; export interface BashCellMeta { toolKind: "bash"; command: string; cwd: string; exitCode?: number | null; durationMs?: number; timedOut?: boolean; } interface BashCellProps { meta: BashCellMeta; /** Raw tool output string from the agent. */ output?: string; isRunning: boolean; } /** Lines shown before "show all" is offered. */ const MAX_VISIBLE_LINES = 500; function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`; } export function BashCell({ meta, output, isRunning }: BashCellProps) { const [expanded, setExpanded] = useState(false); const [showAll, setShowAll] = useState(false); const outputRef = useRef(null); const hasOutput = output && output.trim().length > 0; const canExpand = hasOutput; // Stick to bottom while running useEffect(() => { if (isRunning && expanded && outputRef.current) { outputRef.current.scrollTop = outputRef.current.scrollHeight; } }, [output, isRunning, expanded]); const exitCode = meta.exitCode; const succeeded = exitCode === 0 || exitCode === null || exitCode === undefined; const failed = exitCode !== null && exitCode !== undefined && exitCode !== 0; // Render output lines with optional cap const lines = output ? output.split("\n") : []; const visibleLines = showAll || lines.length <= MAX_VISIBLE_LINES ? lines : lines.slice(lines.length - MAX_VISIBLE_LINES); const hiddenCount = lines.length - visibleLines.length; return (
{/* Header */} {/* Output body */} {expanded && hasOutput && (
{hiddenCount > 0 && !showAll && (
)}
            {visibleLines.join("\n")}
          
)}
); }