import { Box, Text } from 'ink'; import type { AuditTuiState, CommandLogEntry } from '../audit-state'; interface Props { state: AuditTuiState; focused: boolean; /** Total height the pane will occupy. Used to slice to the latest visible lines. */ height: number; } interface Line { key: string; text: string; color?: string; dim?: boolean; } /** * Flatten the command log into renderable lines: a separator * header + stdout/stderr lines + an exit-code footer per entry. * Returned in chronological order (oldest first). */ function flattenLog(log: CommandLogEntry[]): Line[] { const lines: Line[] = []; for (const entry of log) { const time = new Date(entry.startedAt).toLocaleTimeString(); lines.push({ key: `${entry.startedAt}-hdr`, text: `─── ${time} · ${entry.cmd} ───`, dim: true, }); for (let i = 0; i < entry.lines.length; i++) { const ln = entry.lines[i]; lines.push({ key: `${entry.startedAt}-${i}`, text: ln.text, color: ln.stream === 'stderr' ? 'red' : undefined, }); } lines.push({ key: `${entry.startedAt}-exit`, text: entry.exitCode === null ? 'running…' : `exit ${entry.exitCode}`, color: entry.exitCode === 0 ? 'green' : entry.exitCode === null ? undefined : 'red', dim: true, }); } return lines; } export function CommandLogPane({ state, focused, height }: Props) { const all = flattenLog(state.commandLog); // chrome = 2 border rows + 1 title + 1 spacer = 4. Show the last // `height - chrome` lines so the most recent output is always // visible (terminal-tail behavior). const chrome = 4; const visible = Math.max(height - chrome, 0); const slice = all.length <= visible ? all : all.slice(all.length - visible); const hidden = all.length - slice.length; return ( 5 Command log {hidden > 0 ? ` (${hidden} earlier line${hidden === 1 ? '' : 's'} hidden)` : ''} {state.commandLog.length === 0 ? ( (empty — press r on a finding to remediate) ) : ( slice.map((ln) => ( {ln.text} )) )} ); }