import { LitElement, html, css } from 'https://unpkg.com/lit@latest/index.js?module'; // Header component with ASCII art class VibeHeader extends LitElement { static styles = css` :host { display: block; font-family: 'Courier New', monospace; color: var(--terminal-green); margin: 1em 0; } .ascii-art { font-size: 12px; line-height: 1em; white-space: pre; margin: 1em 0; } .title { font-size: 24px; margin: 1em 0; } `; render() { return html`
Terminal Multiplexer Web Interface
`; } } // Session card component class SessionCard extends LitElement { static properties = { session: { type: Object } }; static styles = css` :host { display: block; font-family: 'Courier New', monospace; } .card { border: 1px solid var(--terminal-gray); background: var(--terminal-bg); padding: 1em; cursor: pointer; height: 20em; display: flex; flex-direction: column; } .card:hover { border-color: var(--terminal-green); } .header { color: var(--terminal-cyan); margin-bottom: 1em; } .status { color: var(--terminal-yellow); } .status.running { color: var(--terminal-green); } .preview { flex: 1; border: 1px solid var(--terminal-gray); min-height: 12em; position: relative; } .preview-content { position: absolute; top: 0; left: 0; right: 0; bottom: 0; } `; firstUpdated() { this.renderPreview(); } updated(changedProperties) { if (changedProperties.has('session')) { this.renderPreview(); } } renderPreview() { const previewEl = this.shadowRoot.querySelector('.preview-content'); if (!previewEl || !this.session?.lastOutput) return; try { const lines = this.session.lastOutput.trim().split('\n'); if (lines.length > 1) { // Parse asciinema format const castData = []; for (let i = 1; i < lines.length; i++) { if (lines[i].trim()) { try { castData.push(JSON.parse(lines[i])); } catch (e) { // Skip invalid lines } } } if (castData.length > 0) { const cast = { version: 2, width: 80, height: 24, timestamp: Math.floor(Date.now() / 1000) }; AsciinemaPlayer.create({ data: castData, ...cast }, previewEl, { theme: 'asciinema', loop: false, autoPlay: false, controls: false, fit: 'width' }); } } } catch (error) { previewEl.innerHTML = 'No preview available
'; } } handleClick() { this.dispatchEvent(new CustomEvent('session-select', { detail: { sessionId: this.session.id }, bubbles: true })); } render() { if (!this.session) return html``; const command = this.session.metadata?.cmdline?.join(' ') || 'Unknown'; const status = this.session.status || 'unknown'; return html`No active sessions. Create one above to get started.
` : ''} `; } } // Session detail component class SessionDetail extends LitElement { static properties = { sessionId: { type: String }, session: { type: Object } }; static styles = css` :host { display: block; font-family: 'Courier New', monospace; } .header { display: flex; justify-content: space-between; align-items: center; margin: 1em 0; } .terminal { border: 1px solid var(--terminal-gray); min-height: 30em; position: relative; } .input-area { display: flex; gap: 1em; margin: 1em 0; align-items: center; } input { font-family: 'Courier New', monospace; background: var(--terminal-bg); color: var(--terminal-fg); border: 1px solid var(--terminal-gray); padding: 0 1ch; height: 2em; flex: 1; } button { font-family: 'Courier New', monospace; background: var(--terminal-gray); color: var(--terminal-bg); border: 1px solid var(--terminal-gray); padding: 0 1ch; height: 2em; cursor: pointer; min-width: 8ch; } button:hover { background: var(--terminal-fg); } button.primary { background: var(--terminal-green); border-color: var(--terminal-green); } `; constructor() { super(); this.sessionId = null; this.session = null; this.websocket = null; this.player = null; this.castData = []; } updated(changedProperties) { if (changedProperties.has('sessionId') && this.sessionId) { this.loadSession(); this.connectWebSocket(); } } disconnectedCallback() { super.disconnectedCallback(); this.disconnectWebSocket(); } async loadSession() { try { const response = await fetch('/api/sessions'); const sessions = await response.json(); this.session = sessions.find(s => s.id === this.sessionId); } catch (error) { console.error('Failed to load session:', error); } } connectWebSocket() { this.disconnectWebSocket(); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}?session=${this.sessionId}`; this.websocket = new WebSocket(wsUrl); this.websocket.onopen = () => { console.log(`Connected to session ${this.sessionId}`); }; this.websocket.onmessage = (event) => { try { const castEvent = JSON.parse(event.data); this.castData.push(castEvent); this.updatePlayer(); } catch (error) { console.error('Error parsing WebSocket message:', error); } }; this.websocket.onclose = () => { console.log(`Disconnected from session ${this.sessionId}`); }; this.websocket.onerror = (error) => { console.error('WebSocket error:', error); }; } disconnectWebSocket() { if (this.websocket) { this.websocket.close(); this.websocket = null; } } updatePlayer() { const terminalEl = this.shadowRoot.querySelector('.terminal'); if (!terminalEl || this.castData.length === 0) return; terminalEl.innerHTML = ''; try { const cast = { version: 2, width: 80, height: 24, timestamp: Math.floor(Date.now() / 1000) }; this.player = AsciinemaPlayer.create({ data: this.castData, ...cast }, terminalEl, { theme: 'asciinema', loop: false, autoPlay: true, controls: false, fit: 'width' }); } catch (error) { console.error('Error creating player:', error); terminalEl.innerHTML = 'Error loading terminal
'; } } sendInput(event) { event.preventDefault(); const input = this.shadowRoot.querySelector('input[name="input"]'); const value = input.value.trim(); if (!value || !this.websocket || this.websocket.readyState !== WebSocket.OPEN) { return; } this.websocket.send(JSON.stringify({ type: 'input', data: value })); input.value = ''; } goBack() { this.dispatchEvent(new CustomEvent('go-back', { bubbles: true })); } render() { if (!this.session) { return html`Loading session...
`; } const command = this.session.metadata?.cmdline?.join(' ') || 'Unknown'; return html`