import { useEffect, useRef, useState } from 'react' import { api, TaskInfo, wsURLPath } from './api' import { Btn, Spinner } from './ui' // Follows one background task: backlog, then live chunks over a WebSocket, until it exits. export default function TaskOutput({ id, onDone }: { id: string; onDone?: (t: TaskInfo) => void }) { const [task, setTask] = useState(null) const [text, setText] = useState('') const [connected, setConnected] = useState(false) const box = useRef(null) const doneRef = useRef(onDone) doneRef.current = onDone useEffect(() => { setText(''); setTask(null); setConnected(false) api.task(id).then(setTask).catch(() => { /* shown as unknown */ }) const s = new WebSocket(wsURLPath(`/api/tasks/${id}/stream`)) s.binaryType = 'arraybuffer' const dec = new TextDecoder() s.onopen = () => setConnected(true) s.onmessage = (ev) => { if (ev.data instanceof ArrayBuffer) { setText((t) => t + dec.decode(ev.data, { stream: true })); return } try { const m = JSON.parse(ev.data) if (m.type === 'exit') api.task(id).then((t) => { setTask(t); doneRef.current?.(t) }).catch(() => { /* ignore */ }) } catch { /* ignore */ } } return () => s.close() }, [id]) useEffect(() => { if (box.current) box.current.scrollTop = box.current.scrollHeight }, [text]) const running = !task || task.running const failed = !!task && !task.running && (task.exit_code !== 0 || !!task.error) const status = !task ? 'starting…' : task.running ? 'running…' : task.error ? `failed: ${task.error}` : task.exit_code === 0 ? 'finished' : `exited ${task.exit_code}` return (
{running ? : } {task?.title ?? '…'} · {status} {task?.running && api.cancelTask(id)}>Cancel}
{text || (running ? (connected ? 'waiting for output…' : 'connecting…') : '(no output)')}
) }