import React, { useState, useRef, useEffect } from 'react'; import { Terminal as XTerm } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import 'xterm/css/xterm.css'; import { FaPlus, FaTimes, FaRobot } from 'react-icons/fa'; interface Tab { id: number; terminal: XTerm; socket: WebSocket; } const TerminalTabs: React.FC = () => { const [tabs, setTabs] = useState([]); const [activeTab, setActiveTab] = useState(0); const termRefs = useRef>(new Map()); const nextId = useRef(1); const [suggestionInput, setSuggestionInput] = useState(''); const [suggesting, setSuggesting] = useState(false); const createTerminal = () => { const id = nextId.current++; const term = new XTerm({ cursorBlink: true, theme: { background: '#0d1117', foreground: '#c9d1d9', cursor: '#58a6ff', black: '#484f58', red: '#f85149', green: '#3fb950', yellow: '#d29922', blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#c9d1d9', }, fontSize: 13, fontFamily: 'monospace', }); const fit = new FitAddon(); term.loadAddon(fit); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const socket = new WebSocket(protocol + '//' + window.location.host + '/ws/terminal'); term.onData(data => socket.send('exec:' + data)); socket.onmessage = (event) => term.write(event.data); const newTab: Tab = { id, terminal: term, socket }; setTabs(prev => [...prev, newTab]); setActiveTab(id); return { term, socket, fit }; }; useEffect(() => { createTerminal(); }, []); useEffect(() => { const tab = tabs.find(t => t.id === activeTab); if (tab) { const container = termRefs.current.get(tab.id); if (container && !tab.terminal.element) { tab.terminal.open(container); const fitAddon = new FitAddon(); tab.terminal.loadAddon(fitAddon); fitAddon.fit(); window.addEventListener('resize', () => fitAddon.fit()); } } }, [activeTab, tabs]); const removeTab = (id: number) => { const tab = tabs.find(t => t.id === id); if (tab) { tab.socket.close(); tab.terminal.dispose(); setTabs(prev => prev.filter(t => t.id !== id)); if (activeTab === id) { const remaining = tabs.filter(t => t.id !== id); if (remaining.length > 0) setActiveTab(remaining[0].id); } } }; const handleSuggestion = async () => { if (!suggestionInput.trim()) return; setSuggesting(true); try { const resp = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'groq-llama-3.1-8b-instant', messages: [{ role: 'user', content: 'Suggest a terminal command for: ' + suggestionInput + '. Only respond with the command, nothing else.' }], project_path: '/app/sandbox', }), }); const reader = resp.body?.getReader(); let command = ''; while (reader) { const { done, value } = await reader.read(); if (done) break; const text = new TextDecoder().decode(value); const lines = text.split('\n').filter(l => l.startsWith('data: ')).map(l => l.slice(6)); for (const line of lines) { if (line === '[DONE]') break; try { const { token } = JSON.parse(line); command += token; } catch (e) {} } } const tab = tabs.find(t => t.id === activeTab); if (tab) { tab.terminal.write(command + '\r\n'); } } catch (e) {} setSuggesting(false); setSuggestionInput(''); }; return (
{tabs.map(tab => (
setActiveTab(tab.id)} style={{ padding: '4px 12px', cursor: 'pointer', background: activeTab === tab.id ? 'var(--bg-primary)' : 'transparent', borderRight: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: activeTab === tab.id ? 'var(--text-primary)' : 'var(--text-secondary)', }}> Terminal {tab.id} { e.stopPropagation(); removeTab(tab.id); }} style={{ opacity: 0.6 }} />
))}
setSuggestionInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSuggestion()} />
{tabs.map(tab => (
{ if (el) termRefs.current.set(tab.id, el); }} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: activeTab === tab.id ? 'block' : 'none' }} /> ))}
); }; export default TerminalTabs;