/ 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 &mdash; 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>