// Invite / join UI — room code generation, pasting, connection status. // Depends on: CG.peer, CG.session, CG.newRoomCode function RoomChip({ onClick }) { const [, force] = React.useState(0); React.useEffect(() => { const u1 = CG.peer.onStatus(() => force(t => t + 1)); const u2 = CG.session.onState(() => force(t => t + 1)); return () => { u1(); u2(); }; }, []); const s = CG.peer.status(); const seat = CG.session.getSeat(); const myName = CG.session.getMyName() || (seat === 'A' ? 'maya' : 'jordan'); let label, color, dot; if (s.state === 'connected') { label = (seat === 'A' ? 'hosting · ' : 'joined · ') + myName; color = seat === 'A' ? 'var(--ink)' : 'oklch(42% 0.12 240)'; dot = 'var(--green)'; } else if (s.state === 'waiting') { label = 'waiting for partner…'; color = 'var(--ink)'; dot = 'var(--amber, oklch(65% 0.17 75))'; } else if (s.state === 'connecting' || s.state === 'opening' || s.state === 'reconnecting') { label = s.state; color = 'var(--ink-3)'; dot = 'var(--ink-3)'; } else if (s.state === 'error') { label = 'room error'; color = 'var(--red)'; dot = 'var(--red)'; } else { label = 'host or join a room'; color = 'var(--ink)'; dot = 'var(--ink-3)'; } return ( ); } function RoomModal({ open, onClose }) { const [, force] = React.useState(0); React.useEffect(() => { const u1 = CG.peer.onStatus(() => force(t => t + 1)); const u2 = CG.session.onState(() => force(t => t + 1)); return () => { u1(); u2(); }; }, []); const [joinCode, setJoinCode] = React.useState(''); const [hostName, setHostName] = React.useState(() => CG.session.myName || ''); const [guestName, setGuestName] = React.useState(() => CG.session.myName || ''); const [copied, setCopied] = React.useState(false); const lastRoom = CG.session.getLastRoom(); if (!open) return null; const s = CG.peer.status(); const connected = s.state === 'connected'; const names = CG.session.getNames(); const onHost = async () => { if (!CG.hasKey()) { alert('connect your Anthropic key first · click "connect claude"'); return; } const name = hostName.trim(); if (!name) { alert('enter your name first'); return; } await CG.resume.hardReset(); // clear shared + ledger + threads + logs + phase CG.session.setMyName(name); const code = CG.newRoomCode(); await CG.peer.host(code); }; const onResume = async () => { if (!CG.hasKey()) { alert('connect your Anthropic key first · click "connect claude"'); return; } const name = hostName.trim(); if (!name) { alert('enter your name first'); return; } if (!lastRoom) return; CG.session.setMyName(name); CG.session.restoreShared(lastRoom.code); await CG.peer.host(lastRoom.code); }; const onJoin = async () => { const name = guestName.trim(); if (!name) { alert('enter your name first'); return; } const c = joinCode.trim().toLowerCase(); if (!c) return; await CG.resume.hardReset(); // clear stale ledger/threads/shared — host's sync will arrive shortly CG.session.setMyName(name); await CG.peer.join(c); }; const onCopy = async () => { try { await navigator.clipboard.writeText(s.roomCode); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch {} }; const onLeave = () => { CG.peer.disconnect(); }; return (
browser-to-browser over an encrypted data channel. host funds Claude calls with their key · guest never sees it.
{connected ? ( <>