Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
49 lines
2.3 KiB
TypeScript
49 lines
2.3 KiB
TypeScript
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<TaskInfo | null>(null)
|
|
const [text, setText] = useState('')
|
|
const [connected, setConnected] = useState(false)
|
|
const box = useRef<HTMLPreElement>(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 (
|
|
<div className={'task' + (running ? ' running' : failed ? ' failed' : ' done')}>
|
|
<div className="logs-bar">
|
|
{running ? <Spinner /> : <span className={'dot ' + (failed ? 'bad' : 'ok')} />}
|
|
<span>{task?.title ?? '…'}</span>
|
|
<span className="muted">· {status}</span>
|
|
<span className="spacer" />
|
|
{task?.running && <Btn className="btn" onClick={() => api.cancelTask(id)}>Cancel</Btn>}
|
|
</div>
|
|
<pre className="run-out task-out" ref={box}>{text || (running ? (connected ? 'waiting for output…' : 'connecting…') : '(no output)')}</pre>
|
|
</div>
|
|
)
|
|
}
|