import { useEffect, useRef, useState } from 'react' import { Terminal as XTerm } from '@xterm/xterm' import { FitAddon } from '@xterm/addon-fit' import '@xterm/xterm/css/xterm.css' import { api, getToken, type Integration } from '../lib/api' const GOLD = '#C8A434' const TEXT_SECONDARY = '#7A7D85' export default function Terminal() { const [hosts, setHosts] = useState([]) const [activeHostId, setActiveHostId] = useState(null) const [connected, setConnected] = useState(false) const containerRef = useRef(null) const termRef = useRef(null) const fitRef = useRef(null) const wsRef = useRef(null) useEffect(() => { api.listIntegrations().then(({ integrations }) => { setHosts(integrations.filter((i) => i.type === 'ssh')) }) }, []) useEffect(() => { if (!containerRef.current) return const term = new XTerm({ cursorBlink: true, fontSize: 13, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', theme: { background: '#15161A', foreground: '#E8E6E0', cursor: GOLD }, }) const fit = new FitAddon() term.loadAddon(fit) term.open(containerRef.current) fit.fit() termRef.current = term fitRef.current = fit const onResize = () => fit.fit() window.addEventListener('resize', onResize) return () => { window.removeEventListener('resize', onResize) term.dispose() wsRef.current?.close() } }, []) function connect(hostId: number) { wsRef.current?.close() setActiveHostId(hostId) setConnected(false) const term = termRef.current if (!term) return term.reset() term.writeln('Connecting…') const token = getToken() const proto = window.location.protocol === 'https:' ? 'wss' : 'ws' const ws = new WebSocket(`${proto}://${window.location.host}/api/terminal?token=${encodeURIComponent(token ?? '')}`) wsRef.current = ws ws.onopen = () => { ws.send(JSON.stringify({ type: 'connect', integrationId: hostId, cols: term.cols, rows: term.rows })) } ws.onmessage = (event) => { const msg = JSON.parse(event.data) if (msg.type === 'connected') { setConnected(true) term.reset() } else if (msg.type === 'data') { term.write(msg.data) } else if (msg.type === 'error') { term.writeln(`\r\n\x1b[31mError: ${msg.message}\x1b[0m`) setConnected(false) } else if (msg.type === 'closed') { term.writeln('\r\n\x1b[33mConnection closed.\x1b[0m') setConnected(false) } } ws.onclose = () => setConnected(false) term.onData((data) => { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'input', data })) }) term.onResize(({ cols, rows }) => { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'resize', cols, rows })) }) } return (

SSH Hosts

{hosts.length === 0 && (

No SSH integrations configured. Add one in Settings → Integrations.

)}
{hosts.map((h) => ( ))}
{activeHostId ? (connected ? 'Connected' : 'Disconnected') : 'Select a host to connect'}
) }