// 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 (
e.stopPropagation()} className="paper-card" style={{ maxWidth: 540, width: '100%', padding: 28, position: 'relative' }}>
two-browser session

invite your partner into the room.

browser-to-browser over an encrypted data channel. host funds Claude calls with their key · guest never sees it.

{connected ? ( <>
connected
you are {s.role === 'host' ? names.A : names.B} (seat {s.role === 'host' ? 'A · host' : 'B · guest'}) in room {s.roomCode}.
partner: {s.role === 'host' ? (names.B || '—') : (names.A || '—')}
) : s.state === 'waiting' && s.role === 'host' ? ( <>
share this code with {hostName ? 'your partner' : 'the joiner'}
{s.roomCode}
waiting for your partner to join… they open Clarguments on their end and enter this code.
) : s.state === 'error' ? ( <>
{s.error}
) : ( <>
{/* host */}
host a room
you get a code to share. seat A · your Claude key funds both sides.
setHostName(e.target.value)} style={{ marginBottom: 8, fontSize: '0.85rem' }} /> {!CG.hasKey() && (
connect your Claude key first.
)} {lastRoom && CG.hasKey() && (
last session · room {lastRoom.code}
)}
{/* join */}
join a room
your partner shared a code. seat B · no key needed.
setGuestName(e.target.value)} style={{ marginBottom: 8, fontSize: '0.85rem' }} /> setJoinCode(e.target.value)} style={{ fontFamily: 'var(--font-mono)', fontSize: '0.9rem', marginBottom: 8 }} />
)}
); } // Small banner that shows when the peer drops — used inside the app layout function DisconnectBanner() { const [, force] = React.useState(0); React.useEffect(() => CG.peer.onStatus(() => force(t => t + 1)), []); const s = CG.peer.status(); if (s.state !== 'reconnecting') return null; return (
partner disconnected · room is frozen · waiting for reconnect…
); } // Credits chip — only shows for guest when connected function CreditsChip() { const [, force] = React.useState(0); React.useEffect(() => { const id = setInterval(() => force(t => t + 1), 1000); const u = CG.session.onState(() => force(t => t + 1)); return () => { clearInterval(id); u(); }; }, []); if (!CG.session.isGuest()) return null; const c = CG.session.getCreditsInfo(); return (
claude credits · {c.used}/{c.cap}
); } Object.assign(window, { RoomChip, RoomModal, DisconnectBanner, CreditsChip });