import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react' import { useTerminalSession } from '../hooks/useTerminalSession' function formatTime(timestamp) { if (!timestamp) { return 'Idle' } return new Date(timestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', }) } function statusLabel(status) { if (status === 'running') { return 'Running' } if (status === 'failed') { return 'Failed' } if (status === 'exited') { return 'Completed' } return 'Starting' } const TerminalPane = forwardRef(function TerminalPane({ jobId, title, tone, onTelemetryChange }, ref) { const { session, buffer, connectionState, error, lastOutputAt, restart, resize, sendInput, start, stop } = useTerminalSession(jobId) const viewportRef = useRef(null) const scrollRef = useRef(null) useImperativeHandle( ref, () => ({ submit: async (value) => { await sendInput(value, true) }, }), [sendInput], ) useEffect(() => { onTelemetryChange?.({ jobId, session, connectionState, error, lastOutputAt, }) }, [connectionState, error, jobId, lastOutputAt, onTelemetryChange, session]) useEffect(() => { const container = scrollRef.current if (container) { container.scrollTop = container.scrollHeight } }, [buffer]) useEffect(() => { const element = viewportRef.current if (!element) { return undefined } let frameId = 0 const measure = () => { cancelAnimationFrame(frameId) frameId = requestAnimationFrame(() => { const style = getComputedStyle(element) const fontSize = parseFloat(style.fontSize) || 15 const lineHeight = parseFloat(style.lineHeight) || 24 const cols = Math.max(48, Math.floor(element.clientWidth / (fontSize * 0.61))) const rows = Math.max(14, Math.floor(element.clientHeight / lineHeight)) resize(cols, rows) }) } measure() const observer = new ResizeObserver(measure) observer.observe(element) return () => { cancelAnimationFrame(frameId) observer.disconnect() } }, [resize]) const footerMeta = useMemo( () => [ session?.status ? statusLabel(session.status) : 'Connecting', session?.started_at ? `Started ${formatTime(session.started_at)}` : null, session?.exit_code != null ? `Exit ${session.exit_code}` : null, connectionState === 'connected' ? 'WS live' : connectionState, ].filter(Boolean), [connectionState, session], ) return (

{title}

{statusLabel(session?.status)}

{session?.command || 'Waiting for backend session...'}

{session?.cwd || 'No working directory available yet.'}
{buffer || 'Starting session...\n'}
{session?.status === 'running' ?
) }) export default TerminalPane