import { Box } from "ink"; import { memo, useEffect, useState } from "react"; import type { StreamingState } from "../helpers/accumulator"; import { useTerminalWidth } from "../hooks/useTerminalWidth"; import { Text } from "./Text"; interface StreamingOutputDisplayProps { streaming: StreamingState; /** Show "(esc to interrupt)" hint - used by bash mode (LET-7199) */ showInterruptHint?: boolean; } /** * Display component for streaming bash output during execution. * Shows a rolling window of the last 5 lines with elapsed time. */ export const StreamingOutputDisplay = memo( ({ streaming, showInterruptHint }: StreamingOutputDisplayProps) => { const columns = useTerminalWidth(); // Force re-render every second for elapsed timer const [, forceUpdate] = useState(0); useEffect(() => { const interval = setInterval(() => forceUpdate((n) => n + 1), 1000); return () => clearInterval(interval); }, []); const elapsed = Math.floor((Date.now() - streaming.startTime) / 1000); const { tailLines, totalLineCount } = streaming; const hiddenCount = Math.max(0, totalLineCount - tailLines.length); const contentWidth = Math.max(10, columns - 5); const clipToWidth = (text: string): string => { if (text.length <= contentWidth) { return text; } if (contentWidth <= 1) { return "…"; } return `${text.slice(0, contentWidth - 1)}…`; }; const firstLine = tailLines[0]; const interruptHint = showInterruptHint ? " (esc to interrupt)" : ""; if (!firstLine) { return ( {` ⎿ Running... (${elapsed}s)${interruptHint}`} ); } return ( {/* L-bracket on first line - matches ToolCallMessageRich format " ⎿ " */} {" ⎿ "} {clipToWidth(firstLine.text)} {/* Remaining lines with indent (5 spaces to align with content after bracket) */} {tailLines.slice(1).map((line, i) => ( {" "} {clipToWidth(line.text)} ))} {/* Hidden count + elapsed time */} {hiddenCount > 0 && ( {" "}… +{hiddenCount} more lines ({elapsed}s){interruptHint} )} {/* Always show elapsed while running, even if output is short */} {hiddenCount === 0 && ( {" "}({elapsed}s){interruptHint} )} ); }, ); StreamingOutputDisplay.displayName = "StreamingOutputDisplay";