1import React, { useState, useEffect, useRef } from 'react';
2import { Terminal } from 'xterm';
3import { FitAddon } from 'xterm-addon-fit';
4import 'xterm/css/xterm.css';
5import { apiFetch } from '../../lib/api';
7export default function TabTerminal({ agentId, agentUuid }) {
8 const terminalRef = useRef(null);
9 const containerRef = useRef(null);
10 const wsRef = useRef(null);
11 const xtermRef = useRef(null);
12 const fitAddonRef = useRef(null);
14 const [status, setStatus] = useState('initializing');
15 const [error, setError] = useState(null);
16 const [nodeId, setNodeId] = useState(null);
18 // Fetch MeshCentral node ID
20 async function fetchNodeId() {
22 const response = await apiFetch(`/agent/${agentUuid || agentId}/meshcentral-node`);
24 throw new Error('Failed to fetch MeshCentral node ID');
26 const data = await response.json();
27 setNodeId(data.nodeId);
29 console.error('[Terminal] Error fetching node ID:', err);
30 setError(err.message);
35 if (agentUuid || agentId) {
38 }, [agentId, agentUuid]);
40 // Initialize terminal and WebSocket connection
42 if (!nodeId || !containerRef.current) return;
45 const terminal = new Terminal({
48 fontFamily: 'Menlo, Monaco, "Courier New", monospace',
50 background: '#1e1e1e',
51 foreground: '#d4d4d4',
57 const fitAddon = new FitAddon();
58 terminal.loadAddon(fitAddon);
59 terminal.open(containerRef.current);
62 xtermRef.current = terminal;
63 fitAddonRef.current = fitAddon;
66 const token = localStorage.getItem('token');
68 // Connect to MeshCentral terminal WebSocket
69 const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
70 const ws = new WebSocket(
71 `${protocol}://${window.location.host}/api/meshcentral/terminal?nodeId=${nodeId}&token=${token}`
75 console.log('[Terminal] WebSocket connected');
76 setStatus('connected');
77 terminal.write('\r\n*** Connected to MeshCentral Terminal ***\r\n\r\n');
80 ws.onmessage = (event) => {
81 const data = JSON.parse(event.data);
83 if (data.type === 'console') {
84 // Decode base64 output and write to terminal
85 const output = atob(data.data);
86 terminal.write(output);
87 } else if (data.type === 'error') {
88 terminal.write(`\r\n[Error] ${data.message}\r\n`);
89 setError(data.message);
93 ws.onerror = (err) => {
94 console.error('[Terminal] WebSocket error:', err);
96 setError('WebSocket connection error');
97 terminal.write('\r\n*** Connection error ***\r\n');
101 console.log('[Terminal] WebSocket disconnected');
102 setStatus('disconnected');
103 terminal.write('\r\n*** Disconnected from terminal ***\r\n');
106 // Handle terminal input
107 terminal.onData((data) => {
108 if (ws.readyState === WebSocket.OPEN) {
109 // Encode input as base64 and send to backend
110 const encoded = btoa(data);
111 ws.send(JSON.stringify({
118 // Handle window resize
119 const handleResize = () => {
121 if (ws.readyState === WebSocket.OPEN) {
122 ws.send(JSON.stringify({
130 window.addEventListener('resize', handleResize);
135 window.removeEventListener('resize', handleResize);
136 if (ws.readyState === WebSocket.OPEN) {
145 <div className="tab-container">
147 <div className="card" style={{ textAlign: 'center', padding: '2rem' }}>
148 <div style={{ fontSize: '3rem', marginBottom: '1rem' }}>⚠️</div>
149 <h3>Connection Error</h3>
150 <p style={{ color: '#666' }}>{error}</p>
151 <p style={{ color: '#999', fontSize: '0.9rem', marginTop: '1rem' }}>
152 Make sure MeshCentral agent is installed and connected
159 if (status === 'initializing' && !nodeId) {
161 <div className="tab-container">
163 <div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
164 <div className="spinner" style={{ margin: '0 auto 1rem' }}></div>
165 <p>Loading terminal...</p>
172 <div className="tab-container">
173 <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
174 <h2 style={{ margin: 0 }}>Terminal</h2>
175 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
180 backgroundColor: status === 'connected' ? '#4caf50' : status === 'error' ? '#f44336' : '#ff9800'
182 <span style={{ fontSize: '0.9rem', color: '#666', textTransform: 'capitalize' }}>
188 <div className="card" style={{ padding: '0', overflow: 'hidden' }}>