ui: focus board on live work

This commit is contained in:
kami
2026-07-29 23:38:49 +04:00
parent f7027cb9a7
commit ce02c60106
7 changed files with 76 additions and 72 deletions
+3 -2
View File
@@ -8,6 +8,7 @@ import './style.css'
const client=new QueryClient()
const states:TaskState[]=['queued','leased','completed','failed']
const activeStates:TaskState[]=['queued','leased','blocked']
const label:Record<TaskState,string>={queued:'Queued',leased:'In session',blocked:'Blocked',completed:'Complete',failed:'Failed'}
const blockLabel:Record<BlockReason,string>={lease_failure:'Lease failure',worker_offline:'Worker offline',lease_expired:'Expired lease',approval:'Approval needed',handoff_validation:'Handoff validation',operator_block:'Operator block',system_error:'System error',unknown:'Unknown'}
const actionLabel:Record<string,string>={handoff:'Request handoff',release:'Release task',complete:'Complete task',block:'Mark blocked'}
@@ -19,9 +20,9 @@ function Shell({children,onLogout}:{children:React.ReactNode;onLogout:()=>void})
function taskExplanation(task:Task,overview:Overview){const session=(overview.sessions??[]).find(s=>s.capture?.task_id===task.id);if(task.state==='queued'){const online=(overview.workers??[]).filter(w=>w.online);return online.length?`Waiting for a worker to lease it · ${online.length} worker${online.length===1?'':'s'} online`:'No registered worker is currently reachable'}if(task.state==='leased'){if(session?.pending_approval)return 'Waiting for an operator approval';if(task.lease&&new Date(task.lease.until)<new Date())return 'Lease has expired; waiting for reconciliation';if(session?.blocker)return session.blocker;return session?.capture?'Harness session is active':'Lease is active; live capture is unavailable'}if(task.state==='blocked'){const evidence=task.last_session;const observed=evidence?.captured_at||evidence?.checked_at;const suffix=evidence?` · ${evidence.source||'unknown'} observed ${date(observed)}`:'';return (task.blocker||'Unknown — legacy record has no retained blocker or pane evidence')+suffix}if(task.state==='failed')return 'The task needs review before it can be retried';return 'Terminal task record'}
function TaskCard({task,overview}:{task:Task;overview:Overview}){return <Link className="card" to={'/tasks/'+task.id}><b>{task.title||task.id}</b><small><span>{task.project}</span><span className="machine">{task.id.slice(-5)}</span></small><em>{taskExplanation(task,overview)}</em></Link>}
function Board({tasks,overview}:{tasks:Task[];overview:Overview}){const blocked=tasks.filter(t=>t.state==='blocked').sort((a,b)=>new Date(a.blocked_at||0).getTime()-new Date(b.blocked_at||0).getTime());const groups=new Map<BlockReason,Task[]>;for(const task of blocked){const reason=task.block_reason||'unknown';groups.set(reason,[...(groups.get(reason)||[]),task])}return <><div className="board">{states.map(state=>{const lane=tasks.filter(t=>t.state===state);return <section key={state}><div className="lane-head"><span>{label[state]}</span><span className="lane-count">{lane.length}</span></div>{lane.map(t=><TaskCard task={t} overview={overview} key={t.id}/>)}{!lane.length&&<p className="empty-lane">Nothing here. New work will appear in this branch.</p>}</section>})}</div>{blocked.length>0&&<section className="blocked-groups" aria-label="Blocked work by diagnosis"><div className="section-title"><div><h2>Needs attention</h2><p>Blocked work is grouped by diagnosis and ordered oldest first.</p></div><span className="machine">{blocked.length} blocked</span></div><div className="blocked-grid">{[...groups.entries()].map(([reason,group])=><section key={reason}><div className="lane-head"><span>{blockLabel[reason]}</span><span className="lane-count">{group.length}</span></div>{group.map(task=><TaskCard task={task} overview={overview} key={task.id}/>)}</section>)}</div></section>}</>}
function Board({tasks,overview,empty}:{tasks:Task[];overview:Overview;empty:string}){const blocked=tasks.filter(t=>t.state==='blocked').sort((a,b)=>new Date(a.blocked_at||0).getTime()-new Date(b.blocked_at||0).getTime());const groups=new Map<BlockReason,Task[]>;for(const task of blocked){const reason=task.block_reason||'unknown';groups.set(reason,[...(groups.get(reason)||[]),task])}if(!tasks.length)return <section className="empty-panel"><i className="branch-mark"/><h2>No matching work</h2><p>{empty}</p></section>;const boardStates=states.filter(state=>tasks.some(t=>t.state===state));return <><div className="board">{boardStates.map(state=>{const lane=tasks.filter(t=>t.state===state);return <section key={state}><div className="lane-head"><span>{label[state]}</span><span className="lane-count">{lane.length}</span></div>{lane.map(t=><TaskCard task={t} overview={overview} key={t.id}/>)}</section>})}</div>{blocked.length>0&&<section className="blocked-groups" aria-label="Blocked work by diagnosis"><div className="section-title"><div><h2>Needs attention</h2><p>Blocked work is grouped by diagnosis and ordered oldest first.</p></div><span className="machine">{blocked.length} blocked</span></div><div className="blocked-grid">{[...groups.entries()].map(([reason,group])=><section key={reason}><div className="lane-head"><span>{blockLabel[reason]}</span><span className="lane-count">{group.length}</span></div>{group.map(task=><TaskCard task={task} overview={overview} key={task.id}/>)}</section>)}</div></section>}</>}
function OverviewPage(){const q=useQuery({queryKey:['overview'],queryFn:api.overview,refetchInterval:5000}),[createOpen,setCreateOpen]=useState(false);useEffect(()=>{const open=()=>setCreateOpen(true);window.addEventListener('orchestra:new-task',open);return()=>window.removeEventListener('orchestra:new-task',open)},[]);if(q.isLoading)return <main className="page loading"><p className="eyebrow">Dispatch board</p><p>Fetching queue state from the coordinator</p></main>;if(q.error)return <main className="page error" role="alert">Queue unavailable: {String(q.error)}</main>;const d=q.data!,sessions=d.sessions??[],active=d.tasks.filter(t=>t.state==='leased').length,attention=d.tasks.filter(t=>t.state==='blocked'||t.state==='failed').length,approvals=new Set(sessions.filter(s=>s.pending_approval).map(s=>s.capture?.task_id));return <main className="page"><div className="page-heading"><div><p className="eyebrow">Agent dispatch</p><h1>Keep the work moving.</h1></div><div className="heading-actions"><p>Live task state across every connected harness. The board refreshes every five seconds.</p><button onClick={()=>setCreateOpen(true)}>New task <kbd>N</kbd></button></div></div><div className="status-strip" aria-label="Queue summary"><div><span>Tasks</span><b>{d.tasks.length}</b></div><div><span>In session</span><b>{active}</b></div><div className={attention?'attention':''}><span>Needs attention</span><b>{attention}</b></div><div className={approvals.size?'attention':''}><span>Awaiting approval</span><b>{approvals.size}</b></div></div><div className="section-title"><div><h2>Task flow</h2><p>Each card states the current reason it is waiting or running.</p></div><span className="machine" aria-live="polite">SYNCED {new Date(d.updated_at).toLocaleTimeString()}</span></div><Board tasks={d.tasks} overview={d}/>{createOpen&&<Create overview={d} close={()=>setCreateOpen(false)}/>}</main>}
function OverviewPage(){const q=useQuery({queryKey:['overview'],queryFn:api.overview,refetchInterval:5000}),[createOpen,setCreateOpen]=useState(false),[view,setView]=useState<'active'|'history'|'all'>('active'),[project,setProject]=useState(''),[search,setSearch]=useState('');useEffect(()=>{const open=()=>setCreateOpen(true);window.addEventListener('orchestra:new-task',open);return()=>window.removeEventListener('orchestra:new-task',open)},[]);if(q.isLoading)return <main className="page loading"><p className="eyebrow">Dispatch board</p><p>Fetching queue state from the coordinator</p></main>;if(q.error)return <main className="page error" role="alert">Queue unavailable: {String(q.error)}</main>;const d=q.data!,sessions=d.sessions??[],active=d.tasks.filter(t=>t.state==='leased').length,attention=d.tasks.filter(t=>t.state==='blocked'||t.state==='failed').length,approvals=new Set(sessions.filter(s=>s.pending_approval).map(s=>s.capture?.task_id)),projects=[...new Set(d.tasks.map(t=>t.project).filter(Boolean))].sort(),term=search.trim().toLowerCase(),filtered=d.tasks.filter(t=>(!project||t.project===project)&&(!term||[t.id,t.title,t.description,t.project,t.blocker].filter(Boolean).join(' ').toLowerCase().includes(term))).filter(t=>view==='all'||view==='active'?activeStates.includes(t.state):!activeStates.includes(t.state)),history=d.tasks.filter(t=>!activeStates.includes(t.state)).length,empty=view==='active'?'There is no live work right now. Completed and failed records are available in History.':view==='history'?'No completed or failed records match these filters.':'No task records match these filters.';return <main className="page"><div className="page-heading"><div><p className="eyebrow">Agent dispatch</p><h1>Keep the work moving.</h1></div><div className="heading-actions"><p>Live task state across every connected harness. The board refreshes every five seconds.</p><button onClick={()=>setCreateOpen(true)}>New task <kbd>N</kbd></button></div></div><div className="status-strip" aria-label="Queue summary"><div><span>Tasks</span><b>{d.tasks.length}</b></div><div><span>In session</span><b>{active}</b></div><div className={attention?'attention':''}><span>Needs attention</span><b>{attention}</b></div><div className={approvals.size?'attention':''}><span>Awaiting approval</span><b>{approvals.size}</b></div></div><div className="section-title"><div><h2>Task flow</h2><p>{view==='active'?'Live, waiting, and needs-attention work.':view==='history'?'Completed and failed task records.':'All matching task records.'}</p></div><span className="machine" aria-live="polite">SYNCED {new Date(d.updated_at).toLocaleTimeString()}</span></div><div className="board-filters" aria-label="Task board filters"><div className="view-tabs" role="group" aria-label="Task view"><button className={view==='active'?'selected':''} onClick={()=>setView('active')}>Live work</button><button className={view==='history'?'selected':''} onClick={()=>setView('history')}>History {history>0&&<span>{history}</span>}</button><button className={view==='all'?'selected':''} onClick={()=>setView('all')}>All</button></div><label className="sr-only" htmlFor="task-search">Search tasks</label><input id="task-search" value={search} onChange={e=>setSearch(e.target.value)} placeholder="Search tasks"/><label className="sr-only" htmlFor="project-filter">Filter by project</label><select id="project-filter" value={project} onChange={e=>setProject(e.target.value)}><option value="">All projects</option>{projects.map(p=><option value={p} key={p}>{p}</option>)}</select></div><Board tasks={filtered} overview={d} empty={empty}/>{createOpen&&<Create overview={d} close={()=>setCreateOpen(false)}/>}</main>}
function Create({overview,close}:{overview:Overview;close:()=>void}){const qc=useQueryClient(),nav=useNavigate(),first=useRef<HTMLInputElement>(null),[title,setTitle]=useState(''),[description,setDescription]=useState(''),[project,setProject]=useState(''),[capability,setCapability]=useState(''),[advanced,setAdvanced]=useState(''),[formError,setFormError]=useState('');const projects=[...new Set(overview.tasks.map(t=>t.project).filter(Boolean))];const online=overview.workers.filter(w=>w.online);useEffect(()=>{first.current?.focus()},[]);const m=useMutation({mutationFn:()=>{let extra:Record<string,unknown>={};if(advanced.trim()){try{extra=JSON.parse(advanced)}catch{return Promise.reject(new Error('Additional fields must be valid JSON.'))}}return api.create({...extra,source:'web',external_id:crypto.randomUUID(),project,title,description,capability:capability.split(',').map(x=>x.trim()).filter(Boolean)})},onSuccess:(e:any)=>{qc.invalidateQueries({queryKey:['overview']});nav('/tasks/'+e.task_id)}});const submit=(e:React.FormEvent)=>{e.preventDefault();setFormError('');if(title.trim().length<3){setFormError('Give the task a title of at least three characters.');return}if(!project.trim()){setFormError('Choose an existing project or enter its exact project ID.');return}if(description.trim().length<10){setFormError('Add enough immutable instructions for an agent to act safely.');return}m.mutate()};return <div className="modal-backdrop" onMouseDown={close}><section className="create modal" role="dialog" aria-modal="true" aria-labelledby="new-task-title" onMouseDown={e=>e.stopPropagation()}><div className="modal-heading"><div><p className="eyebrow">Dispatch new work</p><h2 id="new-task-title">New task</h2></div><button className="icon-button" onClick={close} aria-label="Close task creation">×</button></div><p>Give the harness a clear outcome and immutable operating instructions.</p><div className={'eligibility '+(!online.length?'warning':'')}><b>{online.length?`${online.length} worker${online.length===1?'':'s'} currently reachable`:'No workers are currently reachable'}</b><span>{online.length?'Orchestra will choose an eligible worker when the task is leased.':'This task will remain queued until a matching worker reconnects.'}</span></div><form onSubmit={submit} noValidate><label>Task title<input ref={first} required value={title} onChange={e=>setTitle(e.target.value)} placeholder="e.g. Add the health endpoint" autoComplete="off"/></label><label>Project <small>Existing projects are suggested; project IDs remain explicit.</small><input required list="projects" value={project} onChange={e=>setProject(e.target.value)} placeholder="Choose or enter a project" autoComplete="off"/><datalist id="projects">{projects.map(p=><option value={p} key={p}/>)}</datalist></label><label>Capabilities <small>Optional, comma-separated. Only use registered capability names.</small><input value={capability} onChange={e=>setCapability(e.target.value)} placeholder="go, docker" autoComplete="off"/></label><label>Immutable instructions<textarea required value={description} onChange={e=>setDescription(e.target.value)} placeholder="Describe the outcome, constraints, and evidence required."/></label><details><summary>Additional accepted task fields</summary><textarea aria-label="Additional task fields JSON" placeholder={'{"parent":"…","inherent_priority":1,"due":"2026-07-30T12:00:00Z"}'} value={advanced} onChange={e=>setAdvanced(e.target.value)}/></details><div className="modal-actions"><button type="button" className="quiet-button" onClick={close}>Cancel</button><button disabled={m.isPending}>{m.isPending?'Creating task…':'Create task'}</button></div>{(formError||m.error)&&<span className="error" role="alert">{formError||String(m.error)}</span>}</form></section></div>}