// Hedge Fund Team overlay — opens a WS to /ws/team, streams events from N // parallel framework analysts + a synthesizer, shows each agent's progress // and final markdown side by side, then the Managing Director's memo. const { useState: tUseState, useEffect: tUseEffect, useRef: tUseRef, useCallback: tUseCallback } = React; function TeamOverlay({ open, onClose, topic, hypothesis }) { const [members, setMembers] = tUseState([]); const [agentState, setAgentState] = tUseState({}); // { role: { status, title, toolCalls, lastTool, markdown, error } } const [synthesis, setSynthesis] = tUseState(null); // Phase 9: 'idle' (just opened) | 'last-memo' (showing stored verdict) // | 'running' | 'synthesizing' | 'done' | 'error' const [phase, setPhase] = tUseState('idle'); const [lastMemo, setLastMemo] = tUseState(null); // {date, topic, body, model, path} const [error, setError] = tUseState(''); const [expanded, setExpanded] = tUseState({}); const wsRef = tUseRef(null); const startRun = tUseCallback(() => { if (!topic && !(hypothesis && hypothesis.question)) { setError('Need a hypothesis question or topic before running the team.'); setPhase('error'); return; } setMembers([]); setAgentState({}); setSynthesis(null); setError(''); setPhase('running'); const proto = location.protocol === 'https:' ? 'wss' : 'ws'; // Phase 2C.5 — append the Supabase JWT as ?token= so the WS handshake // can authenticate (browsers can't set Authorization headers on WS). // tibebReadToken is installed by the boot gate at index.html. const tok = (window.tibebReadToken && window.tibebReadToken()) || ''; const wsUrl = `${proto}://${location.host}/ws/team${tok ? `?token=${encodeURIComponent(tok)}` : ''}`; const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { ws.send(JSON.stringify({ topic: topic || hypothesis.question, hypothesis: hypothesis || null, })); }; ws.onmessage = (ev) => { let event; try { event = JSON.parse(ev.data); } catch { return; } handleEvent(event); }; ws.onerror = () => { setError("Couldn't reach the analyst team right now. Try again in a few seconds."); setPhase('error'); }; ws.onclose = () => { if (phase === 'running' || phase === 'synthesizing') setPhase('done'); }; }, [topic, hypothesis]); tUseEffect(() => { if (!open) { setMembers([]); setAgentState({}); setSynthesis(null); setPhase('idle'); setError(''); setExpanded({}); setLastMemo(null); if (wsRef.current) try { wsRef.current.close(); } catch {} return; } // On open, fetch the most recent Managing Director memo for this topic. // If found, show it as the default view with a 'Run fresh' button. // If not found, auto-start a run (first time on this thesis). const t = topic || (hypothesis && hypothesis.question) || ''; if (!t) { setError('Need a hypothesis question or topic before running the team.'); setPhase('error'); return; } setPhase('idle'); fetch(`/api/team/last-memo?topic=${encodeURIComponent(t)}`) .then(r => r.json()) .then(j => { if (j.found) { setLastMemo(j); setPhase('last-memo'); } else { // No prior memo — kick off a fresh run immediately startRun(); } }) .catch(() => startRun()); return () => { try { wsRef.current?.close(); } catch {} }; }, [open, topic, hypothesis, startRun]); const handleEvent = tUseCallback((event) => { const t = event.type; if (t === 'team_started') { setMembers(event.members || []); const init = {}; const expandedInit = {}; (event.members || []).forEach(m => { init[m.role] = { title: m.title, model: m.model, status: 'queued', toolCalls: 0, lastTool: '', markdown: '', error: '', priorMemories: 0, memoryPath: '', memoryError: '', }; // Auto-expand every agent card on team start so the user sees // each analyst's thinking + final memo without having to click // through. The user explicitly asked: 'I want each individual // agent's thoughts on the thesis' — auto-expand makes that the // default view, not a hidden detail. expandedInit[m.role] = true; }); setExpanded(expandedInit); // Pre-register synthesizer slot too so its memory chip can render if (event.has_synthesizer) { init['managing-director'] = { title: 'Managing Director', model: event.synthesizer_model, status: 'queued', isSynthesizer: true, priorMemories: 0, memoryPath: '', memoryError: '', }; } setAgentState(init); } else if (t === 'agent_memory_loaded') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), priorMemories: event.count || 0 }, })); } else if (t === 'agent_memory_written') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), memoryPath: event.path || '' }, })); } else if (t === 'agent_memory_error') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), memoryError: event.error || '' }, })); } else if (t === 'agent_started') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), title: event.title, model: event.model, status: 'thinking' }, })); } else if (t === 'agent_text') { // Phase 14 — live token streaming. Each delta arrives as a small // string fragment; we append into the agent's `streaming` buffer // so the UI types out the memo in real time before agent_done // replaces it with the final markdown. const delta = event.delta || ''; if (!delta) return; setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), status: 'thinking', streaming: ((s[event.role] || {}).streaming || '') + delta, }, })); } else if (t === 'agent_tool_use') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), toolCalls: ((s[event.role] || {}).toolCalls || 0) + 1, lastTool: `${event.tool}${event.input_summary ? ' · ' + event.input_summary : ''}`, }, })); } else if (t === 'agent_done') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), status: 'done', markdown: event.markdown || '' }, })); } else if (t === 'agent_error') { setAgentState(s => ({ ...s, [event.role]: { ...(s[event.role] || {}), status: 'error', error: event.error || '' }, })); } else if (t === 'synthesis_started') { setPhase('synthesizing'); setAgentState(s => ({ ...s, 'managing-director': { ...(s['managing-director'] || {}), status: 'thinking', model: event.model }, })); } else if (t === 'synthesis_done') { setSynthesis(event.memo || ''); setAgentState(s => ({ ...s, 'managing-director': { ...(s['managing-director'] || {}), status: 'done' }, })); } else if (t === 'synthesis_error') { setError(`Synthesis failed: ${event.error}`); } else if (t === 'team_done') { setPhase('done'); } else if (t === 'error') { setError(event.error || 'Unknown error.'); setPhase('error'); } }, [phase]); if (!open) return null; const statusColor = (s) => ({ queued: 'var(--text-dim)', thinking: 'var(--catalyst)', done: 'var(--accent)', error: 'var(--danger)', }[s] || 'var(--text-dim)'); const modelChip = (model) => { if (!model) return null; const lower = model.toLowerCase(); let label = model, tier = 'sonnet'; if (lower.includes('opus')) { label = 'OPUS 4.7'; tier = 'opus'; } else if (lower.includes('sonnet')) { label = 'SONNET 4.6'; tier = 'sonnet'; } else if (lower.includes('haiku')) { label = 'HAIKU 4.5'; tier = 'haiku'; } else { label = model.replace(/^claude-/, '').toUpperCase(); } return {label}; }; return (
Hedge Fund Team · {phase}
{topic || (hypothesis && hypothesis.question) || '(no topic)'}
{error &&
{error}
} {/* Phase 9 — last-memo view: show the stored Managing Director verdict with a Re-run button. Only shows when phase === 'last-memo'. */} {phase === 'last-memo' && lastMemo && (
Last verdict · {lastMemo.date} {lastMemo.model && modelChip(lastMemo.model)}
{lastMemo.path}
)}
{members.map(m => { const s = agentState[m.role] || {}; const open = !!expanded[m.role]; return (
setExpanded(e => ({ ...e, [m.role]: !open }))}>
{m.title}
{modelChip(s.model || m.model)}
{s.priorMemories > 0 && ( 📘 {s.priorMemories} prior )} {s.memoryPath && ( ✓ saved )} {s.status === 'thinking' && s.lastTool && {s.lastTool}} {s.toolCalls > 0 && {s.toolCalls} tool{s.toolCalls === 1 ? '' : 's'}} {(s.status || 'queued').toUpperCase()}
{/* Live token stream while the analyst is thinking. Each agent_text event from the backend appends a delta into s.streaming — this renders progressively, so the memo types out in real time. Once agent_done fires, the streaming buffer is replaced with the parsed markdown block below. */} {open && !s.markdown && !s.error && s.streaming && (
', }}/> )} {/* Pre-stream placeholder: the agent has been kicked off (status thinking) but no text has arrived yet. Or it's still queued for a slot. */} {open && !s.markdown && !s.error && !s.streaming && (s.status === 'thinking' || s.status === 'queued') && (
{s.status === 'queued' ? `${m.title} queued — waiting for slot…` : `${m.title} reading the thesis through their lens${s.lastTool ? ` · last tool: ${s.lastTool}` : '…'}`}
)} {open && s.markdown && ( <>
{/* Per-agent re-think button. Closes the user's "AGAINST THEIR PERSONALITY AND ANY RESEARCH IF NEW NODES THEN THEY SHOULD UPDATE THEIR NOTES" loop: runs JUST this analyst against the latest brain + their prior memo, streams the new memo. */}
)} {open && s.error && (
{s.error}
)}
); })}
{(phase === 'synthesizing' || synthesis) && (
{synthesis ? 'Managing Director · memo' : 'Managing Director · synthesizing…'} {modelChip(agentState['managing-director']?.model)} {agentState['managing-director']?.priorMemories > 0 && ( 📘 {agentState['managing-director'].priorMemories} prior )} {agentState['managing-director']?.memoryPath && ( ✓ saved )}
{synthesis && (
)}
)}
{members.filter(m => agentState[m.role]?.status === 'done').length}/{members.length} analysts complete
Analyst team · debate mode
); } window.TeamOverlay = TeamOverlay;