You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
5.5 KiB
JavaScript

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