// terminal.js — PTY 기반 터미널(xterm) — 코드 채팅 탭 내 터미널 포함. // extracted from app.js (v4.3.19-3 split). function v2LiveCodeUpdate() {} function codeEditorWriteFile() {} // ── Terminal infrastructure (PTY over WebSocket) ───────────────────────── let termWs = null; let termWsConnected = false; function termWsConnect() { if (termWs && (termWs.readyState === WebSocket.OPEN || termWs.readyState === WebSocket.CONNECTING)) return; const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; let url = `${proto}//${location.host}/ws`; const token = sessionStorage.getItem('smallclaw_token') || getCookie('smallclaw_session'); if (token) url += `?token=${encodeURIComponent(token)}`; try { termWs = new WebSocket(url); } catch { return; } termWs.onopen = () => { termWsConnected = true; for (const [shellId, inst] of xtermInstances) { if (!inst.ptyReady) { inst.ptyReady = true; termWsSend({ type: 'pty_spawn', shellId, cols: inst.term.cols, rows: inst.term.rows }); } } }; termWs.onmessage = (ev) => { let msg; try { msg = JSON.parse(ev.data); } catch { return; } if (msg.type === 'pty_output') { const i = xtermInstances.get(msg.shellId); if (i) i.term.write(msg.data); } else if (msg.type === 'pty_exit') { const i = xtermInstances.get(msg.shellId); if (i) { i.ptyReady = false; i.term.write('\r\n\x1b[90m[exited]\x1b[0m\r\n'); } } else if (msg.type === 'pty_error') { const i = xtermInstances.get(msg.shellId); if (i) i.term.write('\r\n\x1b[31mPTY error: '+(msg.error||'?')+'\x1b[0m\r\n'); } }; termWs.onclose = () => { termWsConnected = false; termWs = null; for (const [,i] of xtermInstances) i.ptyReady = false; setTimeout(termWsConnect, 3000); }; termWs.onerror = () => { termWsConnected = false; }; } function termWsSend(msg) { if (termWs && termWs.readyState === WebSocket.OPEN) { termWs.send(JSON.stringify(msg)); return true; } return false; } const xtermInstances = new Map(); function createXtermShell(container, shellId) { const id = shellId || 'sh_' + Math.random().toString(36).slice(2, 8); const sub = document.createElement('div'); sub.id = 'xterm-' + id; sub.style.cssText = 'position:absolute;inset:0'; container.appendChild(sub); const term = new Terminal({ theme: { background:'#0C0C0C', foreground:'#CCCCCC', cursor:'#22c55e', selectionBackground:'rgba(34,197,94,0.3)', red:'#E74856', green:'#16C60C', yellow:'#C19C00', blue:'#3B78FF', cyan:'#61D6D6', white:'#CCCCCC', brightBlack:'#767676', brightGreen:'#69FF69', brightWhite:'#FFFFFF' }, fontFamily: "'IBM Plex Mono','Courier New',monospace", fontSize: 13, lineHeight: 1.4, cursorBlink: true, scrollback: 5000, }); const fitAddon = new FitAddon.FitAddon(); const webLinksAddon = new WebLinksAddon.WebLinksAddon(); term.loadAddon(fitAddon); term.loadAddon(webLinksAddon); term.open(sub); fitAddon.fit(); const inst = { term, fitAddon, ptyReady: false }; xtermInstances.set(id, inst); term.onData(data => { if (termWsConnected && inst.ptyReady) termWsSend({ type: 'pty_input', shellId: id, data }); }); term.onResize(({ cols, rows }) => { if (termWsConnected && inst.ptyReady) termWsSend({ type: 'pty_resize', shellId: id, cols, rows }); }); if (termWsConnected) { inst.ptyReady = true; termWsSend({ type: 'pty_spawn', shellId: id, cols: term.cols, rows: term.rows }); } return { id, term, fitAddon, inst }; } // Init WebSocket connection on load fetch('/api/status').then(r => r.json()).catch(() => ({})).finally(termWsConnect); // ── Chat terminal (for chat view) ───────────────────────────────────────── let chatTermShells = []; let activeChatTermShellId = null; let chatTermOpen = false; let chatTermHeight = 280; function toggleChatTerminal() { const panel = document.getElementById('chat-terminal-panel'); const btn = document.getElementById('chat-term-btn'); if (!panel) return; chatTermOpen = !chatTermOpen; if (chatTermOpen) { panel.style.display = 'flex'; if (btn) btn.classList.add('active'); if (!chatTermShells.length) { addChatTermShell(); } else { setTimeout(() => { const s = chatTermShells.find(s => s.id === activeChatTermShellId); if (s?.fitAddon) s.fitAddon.fit(); if (s?.term) s.term.focus(); }, 50); } } else { panel.style.display = 'none'; if (btn) btn.classList.remove('active'); } } function renderChatShellTabs() { const bar = document.getElementById('chat-shell-tabs'); if (!bar) return; bar.innerHTML = chatTermShells.map(s => { const a = s.id === activeChatTermShellId; return `