import type { LogEntry } from './LogPanel'; import { Box, Text, useInput, useStdout } from 'ink'; import { useMemo, useState } from 'react'; const LOG_COLORS: Record = { error: 'red', warn: 'yellow', system: 'cyan', info: 'gray', response: 'green', }; interface FullScreenLogViewProps { logs: LogEntry[]; logFilePath?: string; onExit: () => void; } /** * Full-screen log viewer with smooth cursor-style scrolling. * Press Esc or q to exit back to normal view. */ export function FullScreenLogView({ logs, logFilePath, onExit }: FullScreenLogViewProps) { const { stdout } = useStdout(); const terminalHeight = stdout?.rows ?? 24; // Reserve lines for header (3) and footer (2) const viewportHeight = Math.max(5, terminalHeight - 5); const maxScrollPos = Math.max(0, logs.length - viewportHeight); // Track user scroll offset from bottom (0 = at bottom, positive = scrolled up) const [scrollUpOffset, setScrollUpOffset] = useState(0); // Compute effective cursor position - clamp offset to valid range and convert to position const cursorPos = useMemo(() => { const effectiveOffset = Math.min(scrollUpOffset, maxScrollPos); return Math.max(0, maxScrollPos - effectiveOffset); }, [scrollUpOffset, maxScrollPos]); useInput((input, key) => { if (key.escape || (key.ctrl && input === 'q') || input === 'l') { onExit(); return; } // Smooth scrolling (offset from bottom, so up arrow increases offset) if (key.upArrow || input === 'k') { setScrollUpOffset(offset => Math.min(maxScrollPos, offset + 1)); } if (key.downArrow || input === 'j') { setScrollUpOffset(offset => Math.max(0, offset - 1)); } // Page scrolling if (key.pageUp) { setScrollUpOffset(offset => Math.min(maxScrollPos, offset + viewportHeight)); } if (key.pageDown) { setScrollUpOffset(offset => Math.max(0, offset - viewportHeight)); } // Home/End (g = top = max offset, G = bottom = 0 offset) if (input === 'g') { setScrollUpOffset(maxScrollPos); } if (input === 'G') { setScrollUpOffset(0); } }); const visibleLogs = logs.slice(cursorPos, cursorPos + viewportHeight); const scrollPercent = logs.length <= viewportHeight ? 100 : Math.round((cursorPos / maxScrollPos) * 100); return ( {/* Header */} ๐Ÿ“‹ Log Viewer ({logs.length} entries) {logFilePath && File: {logFilePath}} {/* Log content */} {visibleLogs.length === 0 ? ( No logs yet ) : ( visibleLogs.map((log, idx) => ( {log.level === 'response' ? ( โ”€โ”€โ”€ Response โ”€โ”€โ”€ {log.message} ) : ( <> {String(cursorPos + idx + 1).padStart(4)} {log.level !== 'system' && ( {log.level.toUpperCase().padEnd(6)} )} {log.level === 'system' && {''.padEnd(7)}} {log.message} )} )) )} {/* Scrollbar indicator */} {cursorPos > 0 ? 'โ†‘' : ' '} {scrollPercent}% {cursorPos < maxScrollPos ? 'โ†“' : ' '} {/* Footer */} โ†‘/k up ยท โ†“/j down ยท PgUp/PgDn page ยท g/G top/bottom ยท Esc/q/l exit ); }