// Main application orchestrator (async function () { // Check if auth is required const authStatus = await fetch('/api/auth/status').then((r) => r.json()).catch(() => ({ authRequired: true })); if (authStatus.authRequired) { const token = localStorage.getItem('cmux-remote-token'); if (!token) { window.location.href = '/login.html'; return; } } // State let currentWorkspace = null; let currentSurface = null; // Components const ws = new WebSocketClient(); const terminal = new TerminalView(document.getElementById('terminal-output')); const statusDot = document.getElementById('connection-status'); const terminalContainer = document.getElementById('terminal-container'); const sidebar = new Sidebar((wsRef, surfaceRef) => { switchSurface(wsRef, surfaceRef); }); const gestures = new GestureHandler(sidebar); const theme = new ThemeSwitcher(); // QR modal const qrOverlay = document.getElementById('qr-modal-overlay'); const qrBtn = document.getElementById('qr-btn'); const qrCloseBtn = document.getElementById('qr-modal-close'); const qrUrlEl = document.getElementById('qr-modal-url'); function _qrKeyHandler(e) { if (e.key === 'Escape') { closeQrModal(); } else if (e.key === 'Tab') { e.preventDefault(); qrCloseBtn.focus(); } } async function openQrModal() { try { const { url } = await fetch('/api/access-url').then(r => r.json()); qrUrlEl.textContent = url; } catch { qrUrlEl.textContent = window.location.origin; } // Reload QR image so it's always fresh document.getElementById('qr-modal-image').src = '/api/qr.svg?' + Date.now(); qrOverlay.hidden = false; qrCloseBtn.focus(); document.addEventListener('keydown', _qrKeyHandler); } function closeQrModal() { qrOverlay.hidden = true; document.removeEventListener('keydown', _qrKeyHandler); qrBtn.focus(); } qrBtn.addEventListener('click', openQrModal); qrCloseBtn.addEventListener('click', closeQrModal); qrOverlay.addEventListener('click', (e) => { if (e.target === qrOverlay) closeQrModal(); }); const keyboard = new VirtualKeyboard( (text) => { if (currentWorkspace && currentSurface) { ws.sendText(currentWorkspace, currentSurface, text); } }, (key) => { if (currentWorkspace && currentSurface) { ws.sendKey(currentWorkspace, currentSurface, key); } } ); // Keyboard toggle (desktop) const keyboardToggle = document.getElementById('keyboard-toggle'); const virtualKeyboardEl = document.getElementById('virtual-keyboard'); if (window.innerWidth >= 1024) { virtualKeyboardEl.classList.add('collapsed'); keyboardToggle.hidden = false; } keyboardToggle.addEventListener('click', () => { virtualKeyboardEl.classList.toggle('collapsed'); }); window.addEventListener('resize', () => { if (window.innerWidth < 1024) { keyboardToggle.hidden = true; virtualKeyboardEl.classList.remove('collapsed'); } else { keyboardToggle.hidden = false; } }); // Connection events ws.on('connected', () => { statusDot.classList.add('connected'); statusDot.classList.remove('reconnecting'); statusDot.textContent = 'Connected'; terminalContainer.classList.remove('stale'); }); ws.on('disconnected', () => { statusDot.classList.remove('connected'); statusDot.classList.add('reconnecting'); statusDot.textContent = 'Reconnecting...'; terminalContainer.classList.add('stale'); }); ws.on('reconnecting', ({ attempt }) => { statusDot.textContent = `Reconnecting (${attempt})...`; }); ws.on('auth-ok', () => { // Request workspace list after auth ws.listWorkspaces(); }); ws.on('workspaces', (msg) => { sidebar.setWorkspaces(msg.workspaces); // Auto-subscribe to first terminal surface if none selected if (!currentSurface && msg.workspaces.length > 0) { const firstWs = msg.workspaces[0]; for (const pane of firstWs.panes) { for (const surface of pane.surfaces) { if (surface.type === 'terminal') { switchSurface(firstWs.ref, surface.ref); return; } } } // Fallback: first surface of any type if (firstWs.panes.length > 0 && firstWs.panes[0].surfaces.length > 0) { const s = firstWs.panes[0].surfaces[0]; switchSurface(firstWs.ref, s.ref); } } }); ws.on('screen', (msg) => { if (msg.surface === currentSurface) { terminal.setContent(msg.lines); } }); ws.on('screen-diff', (msg) => { if (msg.surface === currentSurface) { terminal.applyDiff(msg.patches); } }); function switchSurface(wsRef, surfaceRef) { // Unsubscribe from current if (currentSurface) { ws.unsubscribe(currentSurface); } currentWorkspace = wsRef; currentSurface = surfaceRef; terminal.clear(); ws.subscribe(wsRef, surfaceRef); sidebar.setActiveSurface(surfaceRef); } // Visibility API: pause polling when hidden document.addEventListener('visibilitychange', () => { if (document.hidden) { if (currentSurface) ws.unsubscribe(currentSurface); } else { if (currentWorkspace && currentSurface) { ws.subscribe(currentWorkspace, currentSurface); } } }); // Periodic workspace refresh setInterval(() => { if (ws.isConnected()) { ws.listWorkspaces(); } }, 10000); // Start ws.connect(authStatus.authRequired); })();