// Agent activity rendering — the transparency layer. // // Three components: // — splits prose vs "★ Insight" segments // — timeline of tool calls / subagents / artifacts // — structured creative-direction brief // All driven from real SSE events; no agent-specific or tool-specific hardcoding. const { useState, useMemo } = React; // (Insight regex parser removed per "no regex for structured output" rule. // If we need an insight component again, add an `emit_insight` MCP tool the // agent calls deliberately, with its own SSE event + component — never // retroactively pattern-match the model's text stream.) // ───────────────────────────────────────────────────────────────────── // ActivityTrail — timeline of tool calls / subagents / artifacts // ───────────────────────────────────────────────────────────────────── function ActivityTrail({ activity }) { // Always render — empty state shows "no tools used this turn" so the user // can distinguish "agent did silent work" from "trail isn't loading." const arr = Array.isArray(activity) ? activity : []; if (arr.length === 0) { return (
Activity · 0
No tools or subagents used in this turn — agent answered from prompt + skills only.
); } // Walk the activity array, building a tree where subagent_start opens an // indented child group and subagent_stop closes it. const root = []; const stack = [{ children: root, agent_type: null }]; for (const e of arr) { if (e.kind === 'subagent_start') { const node = { kind: 'subagent', agent_type: e.agent_type, agent_id: e.agent_id, children: [] }; stack[stack.length - 1].children.push(node); stack.push({ children: node.children, agent_type: e.agent_type }); } else if (e.kind === 'subagent_stop') { if (stack.length > 1) stack.pop(); } else { stack[stack.length - 1].children.push({ kind: 'leaf', entry: e }); } } return (
Activity · {arr.length}
); } function ActivityNodes({ nodes, depth }) { return (
{nodes.map((n, i) => n.kind === 'subagent' ? : )}
); } function SubagentBlock({ node, depth }) { const [open, setOpen] = useState(true); const label = node.agent_type ? node.agent_type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) : 'subagent'; return (
{open && node.children.length > 0 && (
)}
); } function ActivityLeaf({ entry }) { if (entry.kind === 'tool') return ; if (entry.kind === 'artifact') return ; return null; } function ToolEntry({ entry }) { const [open, setOpen] = useState(false); const hasResult = entry.result !== null && entry.result !== undefined; const isJson = useMemo(() => { if (!hasResult) return false; const r = (entry.result || '').trim(); return r.startsWith('{') || r.startsWith('['); }, [hasResult, entry.result]); const prettyResult = useMemo(() => { if (!hasResult) return ''; if (!isJson) return entry.result; try { return JSON.stringify(JSON.parse(entry.result), null, 2); } catch { return entry.result; } }, [hasResult, isJson, entry.result]); return (
{open && (
{entry.description && (
{entry.description}
)} {hasResult ? (
{prettyResult}
) : (
No result yet.
)}
)}
); } function ActivityArtifactEntry({ entry }) { if (entry.kind_subtype === 'cd_brief') return ; if (entry.kind_subtype === 'ask_user_question') return ; return ; } const ArtifactEntry = ActivityArtifactEntry; // ───────────────────────────────────────────────────────────────────── // AskUserQuestionCard — UI for the SDK's built-in AskUserQuestion tool // ───────────────────────────────────────────────────────────────────── // // The CD calls AskUserQuestion → backend's can_use_tool callback pauses // the SDK query and pushes the structured questions to SSE. This card // renders them as a UI element. On submit, POSTs to /chat/v2/answer // which resolves the backend's pending future, which lets the SDK resume // streaming on the SAME open SSE connection (no new turn). // // Supports 1-4 questions per card with 2-4 options each, multiSelect and // single-select. After submission, the card locks (visual confirmation + // disabled controls). function AskUserQuestionCard({ data }) { // selections: { [questionText]: string | string[] } const [selections, setSelections] = useState({}); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); if (!data || !Array.isArray(data.questions) || data.questions.length === 0) { return null; } const allAnswered = data.questions.every(q => { const s = selections[q.question]; if (q.multiSelect) return Array.isArray(s) && s.length > 0; return typeof s === 'string' && s.length > 0; }); const togglePick = (q, label) => { if (submitted) return; setSelections(prev => { const next = { ...prev }; if (q.multiSelect) { const arr = Array.isArray(next[q.question]) ? [...next[q.question]] : []; const i = arr.indexOf(label); if (i >= 0) arr.splice(i, 1); else arr.push(label); next[q.question] = arr; } else { next[q.question] = label; } return next; }); }; const submit = async () => { if (!allAnswered || submitted) return; // Build the answers map per the SDK's expected shape: // { [questionText]: "Selected Label" } (single-select) // { [questionText]: "Label A, Label B" } (multi-select; comma-joined) const answers = {}; for (const q of data.questions) { const s = selections[q.question]; if (q.multiSelect) { answers[q.question] = (Array.isArray(s) ? s : []).join(', '); } else { answers[q.question] = s || ''; } } try { const res = await fetch('http://localhost:8001/chat/v2/answer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ thread_id: data.thread_id, qid: data.qid, answers }), }); if (!res.ok) { const errBody = await res.json().catch(() => ({})); throw new Error(errBody.detail || `HTTP ${res.status}`); } setSubmitted(true); } catch (e) { setError(e.message || 'Failed to submit answer'); console.error('AskUserQuestion submit failed', e); } }; return (
Pick to continue {submitted && ↳ sent}
{data.questions.map((q, qi) => { const sel = selections[q.question]; return (
{q.header && (
{q.header}{q.multiSelect ? ' · multi' : ''}
)} {q.question && (
{q.question}
)}
{(q.options || []).map((opt, oi) => { const label = opt && opt.label ? opt.label : ''; const desc = opt && opt.description ? opt.description : ''; const active = q.multiSelect ? Array.isArray(sel) && sel.includes(label) : sel === label; return ( ); })}
); })} {!submitted && (
{error && ( {error} )}
)}
); } // ───────────────────────────────────────────────────────────────────── // CDBriefCard — structured creative-direction brief // ───────────────────────────────────────────────────────────────────── function CDBriefCard({ brief }) { const [open, setOpen] = useState(false); if (!brief || typeof brief !== 'object') return null; const vision = brief.vision || {}; const specs = brief.specifications || {}; const refs = brief.references || {}; return (
{open && (
{vision.story_being_told && } {vision.emotional_register && } {Array.isArray(vision.exclusions) && vision.exclusions.length > 0 && ( )} {specs.subject_quality && } {specs.casting_logic && ( )} {specs.environment && specs.environment.place_specificity && ( )} {specs.light && ( )} {Array.isArray(specs.second_look_details) && specs.second_look_details.length > 0 && ( )} {specs.calibrated_risk && } {Array.isArray(refs.cross_domain_pulled) && refs.cross_domain_pulled.length > 0 && (
References pulled
    {refs.cross_domain_pulled.map((r, i) => (
  • {r.reference} {r.for_what_quality && — {r.for_what_quality}}
  • ))}
)} {Array.isArray(refs.in_category_avoided) && refs.in_category_avoided.length > 0 && ( )} {Array.isArray(brief.open_questions) && brief.open_questions.length > 0 && ( )}
)}
); } function CDSection({ label, value }) { return (
{label}
{value}
); } function CDList({ label, items }) { return (
{label}
    {items.map((it, i) =>
  • {it}
  • )}
); } function GenericArtifact({ entry }) { const [open, setOpen] = useState(false); return (
{open && (
{JSON.stringify(entry.data, null, 2)}
)}
); } function formatBytes(n) { if (n == null) return ''; if (n < 1024) return `${n}B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`; return `${(n / 1024 / 1024).toFixed(1)}MB`; } // ───────────────────────────────────────────────────────────────────── // TodoListCard — SDK's TodoWrite live progress // ───────────────────────────────────────────────────────────────────── // // The SDK auto-creates todos for complex multi-step tasks. Each TodoWrite // invocation emits the FULL updated list (replacement, not delta). The // list contains items like: // { content: "Read the brand graph", activeForm: "Reading the brand graph", // status: "pending" | "in_progress" | "completed" } // // We render at the TOP of the assistant message so the user sees the plan // + live progress before reading anything else. function TodoListCard({ todos }) { if (!Array.isArray(todos) || todos.length === 0) return null; const completed = todos.filter(t => t && t.status === 'completed').length; const inProgress = todos.filter(t => t && t.status === 'in_progress').length; const total = todos.length; const fullyDone = completed === total; return (
Plan · {completed}/{total} {inProgress > 0 && ( <> working )}
    {todos.map((t, i) => { if (!t) return null; const isDone = t.status === 'completed'; const isActive = t.status === 'in_progress'; const text = isActive ? (t.activeForm || t.content || '') : (t.content || ''); return (
  • {isDone ? '✓' : (isActive ? '●' : '')} {text}
  • ); })}
); } // Expose to window for views.jsx (Babel-standalone runs files as scripts). window.ActivityTrail = ActivityTrail; window.CDBriefCard = CDBriefCard; window.AskUserQuestionCard = AskUserQuestionCard; window.TodoListCard = TodoListCard;