import { useEffect, useRef, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { api } from '../api/client' import type { Capture, Detail } from '../api/types' import { Chip, Dot, Empty, EndpointGap, M, Panel } from '../components/Primitives' import { Icon } from '../components/Icon' import './Terminal.css' /** This is the screen that must not lie. Everything inside the frame is the * pane's own bytes; everything outside it is state Orchestra can prove. */ function ago(at?: string) { if (!at) return '—' const seconds = Math.max(0, Math.round((Date.now() - new Date(at).getTime()) / 1000)) if (seconds < 60) return `${seconds}s ago` if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago` return `${Math.floor(seconds / 3600)}h ago` } function clock(at?: string) { return at ? new Date(at).toISOString().slice(11, 19) : '—' } function until(at?: string) { if (!at) return '—' const seconds = Math.round((new Date(at).getTime() - Date.now()) / 1000) if (seconds <= 0) return 'expired' const m = Math.floor(seconds / 60) return `${String(m).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}` } function Field({ label, value }: { label: string; value: React.ReactNode }) { return (
{label} {value}
) } export function Terminal() { const { id = '' } = useParams() const queries = useQueryClient() const detail = useQuery({ queryKey: ['detail', id], queryFn: () => api.detail(id), refetchInterval: 3000, }) // The last frame we actually received, kept across refreshes so a lost pane // still shows what was true when it was lost instead of an empty box. const [frame, setFrame] = useState() const [lostAt, setLostAt] = useState() const [copied, setCopied] = useState(false) const [pinned, setPinned] = useState(true) const view = useRef(null) const task = detail.data?.task const session = detail.data?.session const capture = session?.capture useEffect(() => { if (capture) { setFrame(capture) setLostAt(undefined) return } // A leased task with no capture is a lost pane, not an empty one. if (detail.isSuccess && task?.state === 'leased') { setLostAt((was) => was ?? new Date().toISOString()) } }, [capture, detail.isSuccess, task?.state]) // Scrollback inspection must not disturb the live view: we only follow the // tail while the operator is already sitting at the tail. useEffect(() => { const el = view.current if (el && pinned) el.scrollTop = el.scrollHeight }, [frame?.revision, pinned]) const resubmit = useMutation({ mutationFn: () => api.action(id, 'resubmit'), onSuccess: (next) => queries.setQueryData(['detail', id], next), }) const canResubmit = detail.data?.actions.find((a) => a.id === 'resubmit')?.enabled ?? false const lost = Boolean(lostAt) && Boolean(frame) const status = session?.agent_status || task?.pane_state || 'unknown' const health = lost ? 'error' : status === 'blocked' ? 'degraded' : status ? 'healthy' : 'unknown' if (detail.isLoading) { return (

Terminal

Reading the pane…
) } if (detail.isError || !task) { return (

Terminal

{id} is not in the store, so there is no pane to show.

) } return (

Terminal — live pane

{lost ? Pane lost : capture ? Live : No session} {session?.harness_id || task.last_harness_id || 'no worker'} / {session?.pane_id || task.last_pane_id || 'no pane'}
Read-only
{session?.pane_id || task.last_pane_id || '—'} rev {frame ? frame.revision : '—'} {frame?.source || '—'} {clock(frame?.at)}
{frame ? (
 {
                const el = event.currentTarget
                setPinned(el.scrollHeight - el.scrollTop - el.clientHeight < 24)
              }}
            >
              {frame.text}
            
) : (

This task holds no leased session, so no worker is publishing pane text for{' '} {task.id}.

)}
{frame?.truncated && frame truncated by the worker} {!pinned && ( )}
{lost && (

The frame above is the last one Orchestra received. It is history, not the present.

{clock(frame?.at)} UTC} /> {clock(lostAt)} UTC} /> the lease still runs until {clock(task.lease.until)} UTC; nothing is rerouted before it expires ) : ( no lease is held, so this task can be routed again immediately ) } />
)}
{task.id}} /> {session?.harness_id || task.last_harness_id || '—'}} /> {session?.pane_id || task.last_pane_id || '—'}} /> {task.lease?.epoch || '—'}} /> {until(task.lease.until)} left · until {clock(task.lease.until)} UTC ) : ( none ) } /> {frame?.source || '—'}} /> {frame ? frame.revision : '—'}} /> {ago(frame?.at)}} /> read-only} />
{status} } /> {ago(task.last_session?.checked_at || frame?.at)}} /> {(session?.blocker || task.blocker) && ( {session?.blocker || task.blocker}} /> )}

Presses Enter once on input Orchestra placed in this pane. It sends no other key and changes no lifecycle state.

{resubmit.isError && (

{(resubmit.error as Error).message}

)}
{(detail.data?.events ?? []).slice(-5).reverse().map((event) => (
{clock(event.at)} {event.type}
))} {!detail.data?.events.length &&

No events recorded yet.

}
) }