import { Fragment, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { api } from '../api/client' import type { Event, Task } from '../api/types' import { Chip, Empty, EndpointGap, M, Panel } from '../components/Primitives' import './Decisions.css' /** The payload of a HumanDecisionRecorded event. There is no decisions * endpoint, so this screen reduces the log the way domain.ReduceIntent does: * a decision stands until something names it. */ interface DecisionPayload { decision_id?: string kind?: string subject?: string value?: string supersedes?: string[] source?: { provider?: string; external_id?: string } } /** The coordinator serves these on the task; api/types.ts does not model them * yet, so the shapes this screen needs are narrowed here rather than assumed. */ interface BlockedTask extends Task { work_phase?: string decision_request?: { question?: string; why?: string } } interface Decision { id: string taskId: string kind: string subject: string value: string supersedes: string[] provider: string externalId: string at: string eventId: string seq?: number } /** A question the operator has not answered yet. It is not a decision — it * has no id and no value — but it is the same authority, pending. */ interface Waiting { taskId: string question: string why: string at: string } function payload(event: Event): DecisionPayload { return (event.payload ?? {}) as DecisionPayload } function stamp(at: string) { return at.replace('T', ' ').slice(0, 19) + ' UTC' } function shortId(id: string) { return id.length > 8 ? id.slice(-6) : id } interface Reduced { decisions: Decision[] /** decision id -> the decision that retired it, or '' for a standalone * HumanDecisionSuperseded event with no successor. */ retiredBy: Map events: Map } function reduce(events: Event[]): Reduced { const decisions: Decision[] = [] const retiredBy = new Map() const byDecision = new Map() const touch = (id: string, event: Event) => { const list = byDecision.get(id) if (list) list.push(event) else byDecision.set(id, [event]) } for (const event of events) { const p = payload(event) if (!p.decision_id) continue if (event.type === 'HumanDecisionRecorded') { const supersedes = p.supersedes ?? [] decisions.push({ id: p.decision_id, taskId: event.task_id ?? '', kind: p.kind ?? '', subject: p.subject ?? '', value: p.value ?? '', supersedes, provider: p.source?.provider ?? '', externalId: p.source?.external_id ?? '', at: event.at, eventId: event.id, seq: event.seq, }) touch(p.decision_id, event) for (const target of supersedes) { retiredBy.set(target, p.decision_id) touch(target, event) } } else if (event.type === 'HumanDecisionSuperseded') { if (!retiredBy.has(p.decision_id)) retiredBy.set(p.decision_id, '') touch(p.decision_id, event) } } decisions.sort((a, b) => b.at.localeCompare(a.at) || b.id.localeCompare(a.id)) return { decisions, retiredBy, events: byDecision } } function Select({ label, value, options, onChange, }: { label: string value: string options: string[] onChange: (next: string) => void }) { return ( ) } export function Decisions() { const events = useQuery({ queryKey: ['events'], queryFn: () => api.events(), refetchInterval: 5000 }) const overview = useQuery({ queryKey: ['overview'], queryFn: api.overview, refetchInterval: 5000 }) const [status, setStatus] = useState('') const [kind, setKind] = useState('') const [project, setProject] = useState('') const [source, setSource] = useState('') const [query, setQuery] = useState('') const [open, setOpen] = useState() const tasks = useMemo(() => { const map = new Map() for (const task of (overview.data?.tasks ?? []) as BlockedTask[]) map.set(task.id, task) return map }, [overview.data]) const { decisions, retiredBy, events: trail } = useMemo( () => reduce(events.data ?? []), [events.data], ) const waiting = useMemo( () => [...tasks.values()] .filter((t) => t.block_reason === 'human_decision' && t.decision_request) .map((t) => ({ taskId: t.id, question: t.decision_request?.question ?? '', why: t.decision_request?.why ?? '', at: t.blocked_at ?? '', })), [tasks], ) const kinds = useMemo( () => [...new Set(decisions.map((d) => d.kind).filter(Boolean))].sort(), [decisions], ) const sources = useMemo( () => [...new Set(decisions.map((d) => d.provider).filter(Boolean))].sort(), [decisions], ) const projects = useMemo( () => [...new Set([...tasks.values()].map((t) => t.project).filter(Boolean))].sort(), [tasks], ) const needle = query.trim().toLowerCase() const matches = (d: Decision) => { const task = tasks.get(d.taskId) const retired = retiredBy.has(d.id) if (status === 'active' && retired) return false if (status === 'superseded' && !retired) return false if (status === 'waiting') return false if (kind && d.kind !== kind) return false if (source && d.provider !== source) return false if (project && task?.project !== project) return false if (needle) { const hay = [d.id, d.taskId, d.subject, d.value, task?.title ?? ''].join(' ').toLowerCase() if (!hay.includes(needle)) return false } return true } const visible = decisions.filter(matches) const visibleWaiting = waiting.filter((w) => { if (status && status !== 'waiting') return false if (kind || source) return false if (project && tasks.get(w.taskId)?.project !== project) return false if (needle) return [w.taskId, w.question, w.why].join(' ').toLowerCase().includes(needle) return true }) const active = decisions.length - retiredBy.size const loading = events.isLoading || overview.isLoading return (

Decisions

Durable human authority: what the operator decided, and what still stands.

Recorded {decisions.length}
Standing {active}
Superseded {retiredBy.size}
Waiting on you {waiting.length}
setQuery(e.target.value)} />
{loading ? (

reading /v1/events…

) : visible.length + visibleWaiting.length === 0 ? (

The log holds {decisions.length} recorded decisions. Clear the filters to see them.

) : (
{visibleWaiting.map((w) => ( ))} {visible.map((d) => { const retired = retiredBy.has(d.id) const successor = retiredBy.get(d.id) const task = tasks.get(d.taskId) const isOpen = open === d.id return ( setOpen(isOpen ? undefined : d.id)} > {isOpen && ( )} ) })}
ID Decision Task Phase Provenance Recorded Status
{w.question}
{w.why}
{shortId(w.taskId)} {tasks.get(w.taskId)?.title ?? ''} {tasks.get(w.taskId)?.work_phase ?? '—'} {w.at ? stamp(w.at) : '—'} waiting
{shortId(d.id)}
{d.value}
{d.subject}
e.stopPropagation()} > {shortId(d.taskId)} {task?.title ?? ''} {task?.work_phase ?? '—'} {d.provider || '—'} {stamp(d.at)} {retired ? ( superseded ) : ( {d.kind || 'active'} )}
Value

{d.value}

Subject

{d.subject || '—'}

Effective authority

{retired ? successor ? 'Retired. It is out of the effective intent for this task.' : 'Retired by an explicit supersede. It no longer binds the agent.' : 'Standing. It is in the effective intent handed to every session of this task.'}

Decision id
{d.id}
Kind
{d.kind}
Task
{d.taskId}
Provenance
{d.provider || 'unknown'}{' '} {d.externalId && · {d.externalId}}
Supersedes
{d.supersedes.length === 0 ? ( none ) : ( d.supersedes.map((s) => ( )) )}
Superseded by
{!retired ? ( none ) : successor ? ( ) : ( explicit supersede event )}
Event sequence {(trail.get(d.id) ?? []).map((e) => (
{e.seq !== undefined ? `#${e.seq}` : '—'} {e.type} {stamp(e.at)} {e.id}
))}
)} {/* Law 5: the mockup's approval columns have no source in the log. */}
) }