// Phase 02 · LIVE intake — real coach conversation + real ledger writes. // // Solo: the user plays party A by default; seat toggle is available at the top bar. // Multiplayer: each seat gets its own intake conversation, firewalled from the other. // // Reads/writes CG.threads[seat] (private conversation) and CG.ledger[seat] (auto observations). // Submit button sets `shared.openings[seat]._intakeSubmitted = true` (stored on shared state // so the partner sees submission status without seeing the contents). window.LiveIntakePhase = function LiveIntakePhase({ seat, onPhase }) { const [, tick] = React.useState(0); React.useEffect(() => CG.session.onState(() => tick(t => t + 1)), []); React.useEffect(() => CG.threads.subscribe(() => tick(t => t + 1)), []); React.useEffect(() => CG.ledger.subscribe(() => tick(t => t + 1)), []); const shared = CG.session.shared || {}; const names = (CG.session.getNames && CG.session.getNames()) || {}; const mp = CG.session.isMultiplayer && CG.session.isMultiplayer(); const otherSeat = seat === 'A' ? 'B' : 'A'; const myName = names[seat] || (seat === 'A' ? 'host' : 'guest'); const otherName = names[otherSeat] || (otherSeat === 'A' ? 'host' : 'guest'); const [thread, setThread] = React.useState([]); const [ledger, setLedger] = React.useState([]); const [input, setInput] = React.useState(''); const [sending, setSending] = React.useState(false); const [error, setError] = React.useState(''); const [doneSignal, setDoneSignal] = React.useState(null); // { ready, good_outcome } const [autoStarted, setAutoStarted] = React.useState(false); const threadBoxRef = React.useRef(null); // Load thread + ledger on mount + when storage changes. React.useEffect(() => { let alive = true; (async () => { const [t, l] = await Promise.all([CG.threads.read(seat), CG.ledger.read(seat)]); if (!alive) return; setThread(t); setLedger(l); })(); return () => { alive = false; }; }, [seat]); React.useEffect(() => { let alive = true; const refresh = async () => { const [t, l] = await Promise.all([CG.threads.read(seat), CG.ledger.read(seat)]); if (!alive) return; setThread(t); setLedger(l); }; const u1 = CG.threads.subscribe(refresh); const u2 = CG.ledger.subscribe(refresh); return () => { alive = false; u1(); u2(); }; }, [seat]); // Auto-scroll React.useEffect(() => { const el = threadBoxRef.current; if (el) el.scrollTop = el.scrollHeight; }, [thread.length, sending]); // Seed the opening coach turn once, if the thread is empty. React.useEffect(() => { if (autoStarted) return; if (thread.length > 0) { setAutoStarted(true); return; } setAutoStarted(true); (async () => { setSending(true); try { const r = await CG.agents.intakeCoach.next(seat, null); setDoneSignal(r.done_signal); } catch (e) { setError(String(e.message || e)); } finally { setSending(false); } })(); }, [seat, thread.length, autoStarted]); const submittedKey = `_intakeSubmitted_${seat}`; const otherSubmittedKey = `_intakeSubmitted_${otherSeat}`; const youSubmitted = !!(shared.openings && shared.openings[seat] && shared.openings[seat]._intakeSubmitted); const otherSubmitted = !!(shared.openings && shared.openings[otherSeat] && shared.openings[otherSeat]._intakeSubmitted); const bothSubmitted = youSubmitted && (mp ? otherSubmitted : true); // Presence: broadcast what this user is doing React.useEffect(() => { let activity; if (bothSubmitted) activity = 'reviewing · ready'; else if (youSubmitted) activity = 'waiting on partner'; else if (sending) activity = 'chatting with coach'; else if (thread.length < 2) activity = 'starting intake'; else activity = 'chatting with coach'; try { CG.session.setCursor && CG.session.setCursor({ activity }); } catch (e) {} }, [youSubmitted, bothSubmitted, sending, thread.length]); const submitIntake = () => { const openings = { ...(shared.openings || { A: null, B: null }) }; const good = (doneSignal && doneSignal.good_outcome) || null; openings[seat] = { ...(openings[seat] || {}), _intakeSubmitted: true, good_outcome: good }; // In solo mode, also mark the partner's side submitted so the UI can proceed. if (!mp) openings[otherSeat] = { ...(openings[otherSeat] || {}), _intakeSubmitted: true }; CG.session.setShared({ openings }); }; const sendTurn = async () => { const text = input.trim(); if (!text || sending || youSubmitted) return; setSending(true); setError(''); const prev = input; setInput(''); try { const r = await CG.agents.intakeCoach.next(seat, text); setDoneSignal(r.done_signal); } catch (e) { setError(String(e.message || e)); setInput(prev); // restore user's draft on failure } finally { setSending(false); } }; return (
phase 02 · pretext · private + parallel · live

two rooms. one conversation with yourself, not them.

nothing posted. your coach asks open questions and quietly writes to your private ledger as you talk.{' '} nobody else sees this — not {otherName}, not the mediator. not now, not later.

{/* LEFT · your intake */}
intake · {myName} · your room
{thread.length === 0 && !sending && (
coach is thinking of a first question…
)} {thread.map((m, i) => (
{m.role === 'coach' ? 'coach' : myName}
{m.content}
))} {sending && (
coach
)} {error && (
error
{error}
)}
your turn · still private