// 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 });