RL_Surrogate_ENV / frontend /src /hooks /useTerminalSession.js
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 { useCallback, useEffect, useRef, useState } from 'react'
import {
createOrAttachSession,
openTerminalSocket,
resizeTerminalSession,
sendTerminalInput,
stopTerminalSession,
} from '../api/terminal'
const BUFFER_LIMIT = 160000
function trimBuffer(text) {
return text.length > BUFFER_LIMIT ? text.slice(-BUFFER_LIMIT) : text
}
export function useTerminalSession(jobId) {
const [session, setSession] = useState(null)
const [buffer, setBuffer] = useState('')
const [connectionState, setConnectionState] = useState('connecting')
const [error, setError] = useState('')
const [lastOutputAt, setLastOutputAt] = useState(null)
const socketRef = useRef(null)
const resizeRef = useRef({ cols: null, rows: null })
const attachSocket = useCallback((sessionId) => {
if (socketRef.current) {
socketRef.current.close()
}
const socket = openTerminalSocket(sessionId)
socketRef.current = socket
setConnectionState('connecting')
socket.addEventListener('open', () => {
setConnectionState('connected')
})
socket.addEventListener('message', (event) => {
const payload = JSON.parse(event.data)
if (payload.type === 'snapshot') {
setSession(payload.session)
setBuffer(payload.buffer || '')
return
}
if (payload.type === 'output') {
setLastOutputAt(Date.now())
setBuffer((previous) => trimBuffer(previous + payload.data))
return
}
if (payload.type === 'exit') {
setSession((previous) =>
previous
? {
...previous,
status: payload.status,
exit_code: payload.exit_code,
finished_at: payload.finished_at,
}
: previous,
)
}
})
socket.addEventListener('close', () => {
setConnectionState('disconnected')
})
socket.addEventListener('error', () => {
setConnectionState('error')
})
}, [])
const bootSession = useCallback(
async (restart = false) => {
try {
setError('')
const payload = await createOrAttachSession(jobId, { restart })
setSession(payload.session)
setBuffer(payload.buffer || '')
attachSocket(payload.session.id)
} catch (caughtError) {
setError(caughtError.message)
setConnectionState('error')
}
},
[attachSocket, jobId],
)
useEffect(() => {
const timeoutId = window.setTimeout(() => {
void bootSession(false)
}, 0)
return () => {
window.clearTimeout(timeoutId)
if (socketRef.current) {
socketRef.current.close()
}
}
}, [bootSession])
const restart = useCallback(() => bootSession(true), [bootSession])
const stop = useCallback(async () => {
if (!session?.id) {
return
}
try {
await stopTerminalSession(session.id)
} catch (caughtError) {
setError(caughtError.message)
}
}, [session])
const sendInput = useCallback(
async (value, appendNewline = true) => {
if (!session?.id || !value.trim()) {
return
}
try {
await sendTerminalInput(session.id, value, appendNewline)
} catch (caughtError) {
setError(caughtError.message)
}
},
[session],
)
const resize = useCallback(
async (cols, rows) => {
if (!session?.id) {
return
}
const previous = resizeRef.current
if (previous.cols === cols && previous.rows === rows) {
return
}
resizeRef.current = { cols, rows }
try {
await resizeTerminalSession(session.id, cols, rows)
} catch {
// Ignore resize errors so rendering stays responsive.
}
},
[session],
)
return {
buffer,
connectionState,
error,
lastOutputAt,
restart,
resize,
sendInput,
session,
start: () => bootSession(false),
stop,
}
}