Spaces:
Configuration error
Configuration error
Aman Nindra
Enhance backend terminal session management and frontend command comparison features. Added new API endpoints for terminal session creation, input handling, resizing, and stopping. Updated frontend to support real-time command broadcasting and display runtime comparisons between jobs. Improved styling and layout for terminal panes and comparison statistics.
4dcc016 | 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 ( | |
| <article className={`terminal-pane terminal-pane--${tone}`}> | |
| <header className="terminal-pane__header"> | |
| <div className="terminal-pane__heading"> | |
| <div className="terminal-pane__title-row"> | |
| <span className="terminal-pane__dot" /> | |
| <h2>{title}</h2> | |
| <span className={`status-chip status-chip--${session?.status || 'starting'}`}> | |
| {statusLabel(session?.status)} | |
| </span> | |
| </div> | |
| <p>{session?.command || 'Waiting for backend session...'}</p> | |
| <small>{session?.cwd || 'No working directory available yet.'}</small> | |
| </div> | |
| <div className="terminal-pane__actions"> | |
| <button type="button" onClick={start}> | |
| Attach | |
| </button> | |
| <button type="button" onClick={restart}> | |
| Restart | |
| </button> | |
| <button type="button" onClick={stop}> | |
| Stop | |
| </button> | |
| </div> | |
| </header> | |
| <div ref={viewportRef} className="terminal-pane__viewport"> | |
| <div ref={scrollRef} className="terminal-pane__scroll"> | |
| <pre className="terminal-pane__buffer">{buffer || 'Starting session...\n'}</pre> | |
| {session?.status === 'running' ? <span className="terminal-pane__cursor" aria-hidden="true" /> : null} | |
| </div> | |
| </div> | |
| <footer className="terminal-pane__footer"> | |
| <div className="terminal-pane__meta"> | |
| {footerMeta.map((item) => ( | |
| <span key={item}>{item}</span> | |
| ))} | |
| {error ? <span className="terminal-pane__error">{error}</span> : null} | |
| </div> | |
| </footer> | |
| </article> | |
| ) | |
| }) | |
| export default TerminalPane | |