import { useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { api } from '../api/client' import type { Overview, Task } from '../api/types' import { Chip, Empty, EndpointGap, M, PHASES, PhasePath } from '../components/Primitives' import { Icon } from '../components/Icon' import './Tasks.css' type GroupKey = 'attention' | 'running' | 'waiting' | 'completed' | 'failed' const GROUPS: { key: GroupKey; title: string; icon: string; open: boolean }[] = [ { key: 'attention', title: 'Needs attention', icon: 'alert', open: true }, { key: 'running', title: 'Running', icon: 'play', open: true }, { key: 'waiting', title: 'Waiting / queued', icon: 'queue', open: true }, { key: 'completed', title: 'Completed', icon: 'check', open: false }, { key: 'failed', title: 'Failed', icon: 'x', open: false }, ] function groupOf(task: Task): GroupKey { switch (task.state) { case 'needs_attention': case 'blocked': return 'attention' case 'leased': return 'running' case 'completed': return 'completed' case 'failed': return 'failed' default: return 'waiting' } } /** The harness holding the lease, or the last one that did. Nothing is * invented: a task that never ran reports no worker. */ function harnessOf(task: Task) { return task.lease?.harness_id || task.last_harness_id || '' } function stateTone(task: Task) { if (task.state === 'failed' || task.state === 'blocked') return 'fault' as const if (task.state === 'needs_attention') return 'warn' as const if (task.state === 'completed') return 'done' as const if (task.state === 'leased') return 'accent' as const return undefined } function ago(at?: string) { if (!at) return '' const ms = Date.now() - Date.parse(at) if (!Number.isFinite(ms)) return '' return `${span(ms)} ago` } function span(ms: number) { const s = Math.max(0, Math.round(ms / 1000)) if (s < 60) return `${s}s` if (s < 3600) return `${Math.round(s / 60)}m` if (s < 86400) return `${Math.round(s / 3600)}h` return `${Math.round(s / 86400)}d` } /** The one time value a task actually carries per state. There is no * per-task updated_at on the overview, so this never claims one. */ function timing(task: Task): { label: string; value: string } | undefined { if (task.lease?.until) { const left = Date.parse(task.lease.until) - Date.now() return left > 0 ? { label: 'lease', value: `${span(left)} left` } : { label: 'lease', value: `expired ${span(-left)} ago` } } if (task.blocked_at) return { label: 'blocked', value: ago(task.blocked_at) } if (task.next_retry_at) { const left = Date.parse(task.next_retry_at) - Date.now() return { label: 'retry', value: left > 0 ? `in ${span(left)}` : 'due' } } if (task.due) return { label: 'due', value: task.due.slice(0, 10) } return undefined } function unique(values: (string | undefined)[]) { return [...new Set(values.filter((value): value is string => !!value))].sort() } function Select({ label, value, options, onChange, }: { label: string value: string options: string[] onChange: (next: string) => void }) { return ( ) } function Row({ task, agentStatus }: { task: Task; agentStatus?: string }) { const harness = harnessOf(task) const when = timing(task) return ( {task.title || task.external_id || 'untitled task'} {task.project} · {task.external_id || task.id} {task.lifecycle_phase || '—'} {harness ? ( <> {harness} {task.last_pane_id && {task.last_pane_id}} ) : ( unassigned )} {when && ( <> {when.label} {when.value} )} {agentStatus && {agentStatus}} {task.block_reason || task.state} ) } function Group({ title, icon, open, tasks, statusFor, }: { title: string icon: string open: boolean tasks: Task[] statusFor: (task: Task) => string | undefined }) { return (
0}>

{title}

{tasks.length}
{tasks.length ? (
{tasks.map((task) => ( ))}
) : ( )}
) } export function Tasks() { const { data, isLoading, error } = useQuery({ queryKey: ['overview'], queryFn: api.overview, refetchInterval: 5000, }) const [project, setProject] = useState('') const [phase, setPhase] = useState('') const [worker, setWorker] = useState('') const [attention, setAttention] = useState('') const [group, setGroup] = useState('') const tasks = useMemo(() => data?.tasks ?? [], [data]) /** Agent status comes from the live session, matched by pane. It is an * agent claim, so it stays a separate chip from orchestra's own state. */ const statusFor = useMemo(() => { const byPane = new Map((data?.sessions ?? []).map((s) => [s.pane_id, s])) return (task: Task) => task.state === 'leased' ? byPane.get(task.last_pane_id)?.agent_status : undefined }, [data]) const filtered = tasks.filter((task) => { if (project && task.project !== project) return false if (phase && task.lifecycle_phase !== phase) return false if (worker && harnessOf(task) !== worker) return false if (attention === 'needs' && groupOf(task) !== 'attention') return false if (attention === 'clear' && groupOf(task) === 'attention') return false if (attention && attention !== 'needs' && attention !== 'clear' && task.block_reason !== attention) return false return true }) const counts = (key: GroupKey) => filtered.filter((task) => groupOf(task) === key).length const shown = GROUPS.filter((g) => !group || g.key === group) return (

Tasks

All work under orchestration.

{tasks.length}
{GROUPS.map((g) => ( ))}
task.block_reason))]} onChange={setAttention} />
{error && (

/v1/ui/overview failed: {(error as Error).message}

)} {isLoading && !data && (

loading /v1/ui/overview…

)} {data && !tasks.length && (

Orchestra has no work on record. Ingest a task to start.

)} {data && tasks.length > 0 && shown.map((g) => ( groupOf(task) === g.key)} statusFor={statusFor} /> ))} {data && tasks.length > 0 && (
)}
) }