// Shell — header, phase nav, viewpoint toggle, layout // Depends on: SCENARIO, Icons, Thermometer, AgentBadge const PHASES = [ { id: 'style', label: '00 · Style', short: '00' }, { id: 'topic', label: '01 · Topic lock', short: '01' }, { id: 'intake', label: '02 · Pretext', short: '02' }, { id: 'opening', label: '03 · Blind opening', short: '03' }, { id: 'steelman', label: '04 · Steel-man gate', short: '04' }, { id: 'room', label: '05 · The room', short: '05' }, { id: 'callout', label: '06 · Pattern callout', short: '06' }, { id: 'apology', label: '07 · Apology on-ramp', short: '07' }, { id: 'artifact', label: '08 · Artifact', short: '08' }, ]; function PhaseNav({ current, onSelect }) { const currentIdx = PHASES.findIndex(p => p.id === current); // Partner presence: read shared.cursor[otherSeat].phase in live MP const [, tick] = React.useState(0); React.useEffect(() => CG.session.onState(() => tick(t => t + 1)), []); const mp = CG.session.isMultiplayer && CG.session.isMultiplayer(); const mySeat = CG.session.getSeat && CG.session.getSeat(); const otherSeat = mySeat === 'A' ? 'B' : mySeat === 'B' ? 'A' : null; const otherCursor = (mp && otherSeat && CG.session.shared?.cursor?.[otherSeat]) || null; const otherPhase = otherCursor && otherCursor.phase || null; const otherActivity = otherCursor && otherCursor.activity || null; const otherIdx = otherPhase ? PHASES.findIndex(p => p.id === otherPhase) : -1; const names = CG.session.getNames ? CG.session.getNames() : { A: null, B: null }; const otherName = otherSeat && names[otherSeat] ? names[otherSeat] : (otherSeat === 'A' ? 'host' : 'guest'); return (
{PHASES.map((p, i) => { const state = i < currentIdx ? 'done' : i === currentIdx ? 'active' : ''; const otherHere = otherIdx === i; return (
onSelect(p.id)} title={p.label} > {p.short}
{otherHere && ( )}
{i < PHASES.length - 1 && (
)} ); })}
{PHASES[currentIdx]?.label.toLowerCase()}
{mp && otherPhase && (
{otherName} · {PHASES[otherIdx]?.short || '??'}{otherActivity ? ` · ${otherActivity}` : ''}
)}
); } function TopBar({ phase, onPhase, seat, onPause, onConnectClaude, onOpenRoom }) { const S = window.SCENARIO; // Live-mode awareness: read topic + room id from session when we have a key + fresh mode const [, forceTick] = React.useState(0); React.useEffect(() => { const u1 = CG.onStatus(() => forceTick(t => t + 1)); const u2 = CG.session.onState(() => forceTick(t => t + 1)); return () => { u1(); u2(); }; }, []); const liveMode = (CG.hasKey() && CG.getMode() === 'fresh') || !!(CG.session && CG.session.isGuest && CG.session.isGuest()); const shared = CG.session.shared || {}; const liveTopic = shared.topic && shared.topic.text ? shared.topic.text : null; const liveAmendment = shared.topic && shared.topic.amendment ? shared.topic.amendment : ''; const bothSigned = shared.topic && shared.topic.signedA && shared.topic.signedB; const names = CG.session.getNames ? CG.session.getNames() : { A: null, B: null }; // Room id: live peer room, falls back to a quiet placeholder in live mode const peerStatus = CG.peer.status(); const liveRoomId = peerStatus && peerStatus.roomCode ? peerStatus.roomCode : null; const displayRoomId = liveMode ? (liveRoomId || 'solo') : S.room.id; const displayTopic = liveMode ? (liveTopic ? liveTopic + (liveAmendment ? ` ${liveAmendment.startsWith('…') ? liveAmendment : '…' + liveAmendment}` : '') : null) : S.topic.refined; const displayCosign = liveMode ? (bothSigned ? `co-signed · ${names.A} · ${names.B}` : liveTopic ? (shared.topic.signedA ? `signed · ${names.A} · waiting on ${names.B}` : `draft · not yet signed`) : null) : `co-signed · maya · jordan — ${S.room.createdAt}`; return (
{/* top row */}
clarguments · room {displayRoomId}
{displayTopic ? (
topic · {displayTopic}
) : (
topic · not yet locked — phase 01 writes here
)} {displayCosign &&
{displayCosign}
}
); } // Style chip — shows status of the phase-00 style co-sign. Only rendered in live mode. function StyleChip() { const [, tick] = React.useState(0); React.useEffect(() => { const u1 = CG.onStatus(() => tick(t => t + 1)); const u2 = CG.session.onState(() => tick(t => t + 1)); return () => { u1(); u2(); }; }, []); const liveMode = (CG.hasKey() && CG.getMode() === 'fresh') || !!(CG.session && CG.session.isGuest && CG.session.isGuest()); if (!liveMode) return null; const style = CG.session && CG.session.getStyle ? CG.session.getStyle() : null; const locked = !!(style && style.signedA && style.signedB); const label = locked ? `style · ${style.voice} · ${style.asymmetry} · ${style.directness}` : style ? 'style · proposed' : 'style · not set'; return (
{label}
); } // (Admin three-agent transparency panel removed — production UI.) Object.assign(window, { PHASES, PhaseNav, TopBar, StyleChip });