import { useCallback, useEffect, useState } from 'react' import { ChevronDown, ChevronRight, Clock, Cpu, ScrollText, ShieldAlert, Wrench } from 'lucide-react' import { toast } from 'sonner' import { AppShell } from '@/components/AppShell' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Label } from '@/components/ui/label' import { Skeleton } from '@/components/ui/skeleton' import { Textarea } from '@/components/ui/textarea' import { REVIEWS_CHANGED } from '@/components/AppShell' import { AgentFace } from '@/components/AgentFace' import { MarkdownEditor } from '@/components/MarkdownEditor' import { api } from '@/lib/api' import { diffWords } from '@/lib/diff' import { useAuth } from '@/store/auth' interface ReviewItem { id: string teamId: string agentRunId: string agentId: string workItemId: string actionKind: string risk: string title: string content: string childTitles: string[] trace: string | null status: string createdAtUtc: string } interface RunDetail { status: string output: string | null prompt: string | null error: string | null trace: string | null resultJson: string | null latencyMs: number | null } interface RunTrace { agent?: string autonomy?: string skills?: string[] tools?: string[] docs?: string[] memories?: number product?: { productId?: string | null; identity?: boolean } task?: { taskType?: string } } interface RunResult { action?: string risk?: string skill?: string toolCalls?: { tool: string; server?: string | null; ok: boolean }[] } function parseJson(value: string | null | undefined): T | null { if (!value) return null try { return JSON.parse(value) as T } catch { return null } } export function ReviewsPage() { const organizationId = useAuth((s) => s.organizationId) const [items, setItems] = useState(null) const load = useCallback(async () => { if (!organizationId) return try { setItems(await api.get(`/api/governance/reviews?organizationId=${organizationId}`)) } catch (err) { toast.error((err as Error).message) setItems([]) } }, [organizationId]) useEffect(() => { void load() }, [load]) return (

Review inbox

Held agent actions awaiting your decision. See the action, the result, and the run log before you approve. Your edits feed the metric.

{items === null && (
)} {items?.length === 0 && ( Nothing is waiting on you. Held agent actions will appear here. )}
{items?.map((item) => ( setItems((s) => s?.filter((x) => x.id !== id) ?? s)} /> ))}
) } function ReviewCard({ item, onDecided }: { item: ReviewItem; onDecided: (id: string) => void }) { const [content, setContent] = useState(item.content) const [childrenText, setChildrenText] = useState(item.childTitles.join('\n')) const [showLog, setShowLog] = useState(false) const [run, setRun] = useState(null) const [busy, setBusy] = useState(false) const destructive = item.risk.toLowerCase() === 'destructive' const childCount = childrenText.split('\n').map((l) => l.trim()).filter(Boolean).length // Lazily pull the full run (latency, tool calls, raw output, assembled prompt) the first time the // approver opens the log. const toggleLog = async () => { const next = !showLog setShowLog(next) if (next && !run) { try { setRun(await api.get(`/api/assembler/runs/${item.agentRunId}`)) } catch { // The run row may be gone; the assembly trace on the item still renders below. } } } async function decide(action: 'approve' | 'sendback') { setBusy(true) try { if (action === 'approve') { const childTitles = childrenText.split('\n').map((line) => line.trim()).filter(Boolean) const result = await api.post<{ editDistance: number | null; decision: string }>( `/api/governance/reviews/${item.id}/approve`, { content, childTitles }, ) const distance = result.editDistance ?? 0 toast.success( result.decision === 'EditedAndApproved' ? `Approved with edits — edit distance ${distance.toFixed(3)}` : 'Approved as proposed', ) } else { await api.post(`/api/governance/reviews/${item.id}/sendback`, {}) toast.info('Sent back to the agent') } onDecided(item.id) window.dispatchEvent(new Event(REVIEWS_CHANGED)) } catch (err) { toast.error((err as Error).message) } finally { setBusy(false) } } return (
{item.title}
{item.actionKind} {destructive && } {item.risk} {new Date(item.createdAtUtc).toLocaleString()}
{/* Action — what approving will actually do. */}
{item.actionKind.replace(/-/g, ' ')} ·{' '} {destructive ? 'Destructive — always held for a human.' : `On approve, this artifact is written to the board${childCount ? ` and ${childCount} child task${childCount === 1 ? '' : 's'} created` : ''}.`}
{/* Result — the proposed artifact + child tasks (both editable). */}
{content !== item.content && (
{diffWords(item.content, content).map((segment, i) => segment.kind === 'same' ? ( {segment.text} ) : segment.kind === 'removed' ? ( {segment.text} ) : ( {segment.text} ), )}
)}