/ web-terminal / index.html
index.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Bob — Claude Terminal</title> 7 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"> 8 <style> 9 :root { --bg: #0a0a14; --surface: #12121e; --accent: #4a6fa5; --dim: #555; } 10 * { margin: 0; padding: 0; box-sizing: border-box; } 11 body { background: var(--bg); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; height: 100vh; display: flex; flex-direction: column; } 12 #header { background: var(--surface); padding: 0.5rem 1rem; display: flex; align-items: center; gap: 1rem; border-bottom: 1px solid #222; } 13 #header h1 { font-size: 0.9rem; color: var(--accent); font-weight: 600; } 14 #header .status { font-size: 0.75rem; color: var(--dim); } 15 #header .status.connected { color: #2ec4b6; } 16 #header .status.disconnected { color: #e63946; } 17 #terminal-container { flex: 1; padding: 4px; } 18 .xterm { height: 100%; } 19 </style> 20 </head> 21 <body> 22 23 <div id="header"> 24 <h1>Bob — Claude Terminal</h1> 25 <span class="status" id="status">Connecting...</span> 26 </div> 27 <div id="terminal-container"></div> 28 29 <script type="module"> 30 import { Terminal } from 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/+esm'; 31 import { FitAddon } from 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/+esm'; 32 import { WebLinksAddon } from 'https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/+esm'; 33 import { WebglAddon } from 'https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/+esm'; 34 35 const statusEl = document.getElementById('status'); 36 const container = document.getElementById('terminal-container'); 37 38 const term = new Terminal({ 39 fontFamily: '"JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono", Menlo, monospace', 40 fontSize: 14, 41 lineHeight: 1.2, 42 cursorBlink: true, 43 cursorStyle: 'bar', 44 allowProposedApi: true, 45 scrollback: 10000, 46 theme: { 47 background: '#0a0a14', 48 foreground: '#c8c8d8', 49 cursor: '#4a6fa5', 50 cursorAccent: '#0a0a14', 51 selectionBackground: '#4a6fa533', 52 black: '#1a1a2e', 53 red: '#e63946', 54 green: '#2ec4b6', 55 yellow: '#f4a261', 56 blue: '#4a6fa5', 57 magenta: '#b388ff', 58 cyan: '#4dd0e1', 59 white: '#c8c8d8', 60 brightBlack: '#555', 61 brightRed: '#ff6b6b', 62 brightGreen: '#69f0ae', 63 brightYellow: '#ffd54f', 64 brightBlue: '#82b1ff', 65 brightMagenta: '#ea80fc', 66 brightCyan: '#84ffff', 67 brightWhite: '#ffffff', 68 }, 69 }); 70 71 const fitAddon = new FitAddon(); 72 term.loadAddon(fitAddon); 73 term.loadAddon(new WebLinksAddon()); 74 75 term.open(container); 76 77 try { 78 const webglAddon = new WebglAddon(); 79 webglAddon.onContextLoss(() => webglAddon.dispose()); 80 term.loadAddon(webglAddon); 81 } catch (e) { 82 console.warn('WebGL not available, using canvas renderer'); 83 } 84 85 fitAddon.fit(); 86 87 // WebSocket 88 const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:'; 89 const wsUrl = `${wsProto}//${location.host}/ws`; 90 let ws = null; 91 let reconnectTimer = null; 92 93 function connect() { 94 ws = new WebSocket(wsUrl); 95 96 ws.onopen = () => { 97 statusEl.textContent = 'Connected'; 98 statusEl.className = 'status connected'; 99 // Send initial resize 100 ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); 101 }; 102 103 ws.onmessage = (e) => { 104 term.write(e.data); 105 }; 106 107 ws.onclose = () => { 108 statusEl.textContent = 'Disconnected — reconnecting...'; 109 statusEl.className = 'status disconnected'; 110 reconnectTimer = setTimeout(connect, 3000); 111 }; 112 113 ws.onerror = () => { 114 ws.close(); 115 }; 116 } 117 118 // Terminal input → WebSocket 119 term.onData((data) => { 120 if (ws && ws.readyState === WebSocket.OPEN) { 121 ws.send(data); 122 } 123 }); 124 125 // Resize → WebSocket 126 const sendResize = () => { 127 fitAddon.fit(); 128 if (ws && ws.readyState === WebSocket.OPEN) { 129 ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })); 130 } 131 }; 132 133 window.addEventListener('resize', sendResize); 134 new ResizeObserver(sendResize).observe(container); 135 136 connect(); 137 term.focus(); 138 </script> 139 </body> 140 </html>