Terminal Integration Examples

This section provides comprehensive examples for integrating with AIO Sandbox's WebSocket terminal interface.

Basic Terminal Client

Simple Node.js Implementation

const WebSocket = require('ws');

class TerminalClient {
    constructor(baseUrl = 'ws://localhost:8080') {
        this.baseUrl = baseUrl;
        this.ws = null;
        this.sessionId = null;
    }
    
    connect(sessionId = null) {
        const wsUrl = `${this.baseUrl}/v1/shell/ws${sessionId ? `?session_id=${sessionId}` : ''}`;
        
        this.ws = new WebSocket(wsUrl);
        
        this.ws.on('open', () => {
            console.log('Connected to terminal');
        });
        
        this.ws.on('message', (data) => {
            const message = JSON.parse(data.toString());
            this.handleMessage(message);
        });
        
        this.ws.on('close', () => {
            console.log('Terminal connection closed');
        });
        
        this.ws.on('error', (error) => {
            console.error('Terminal error:', error);
        });
    }
    
    handleMessage(message) {
        const { type, data } = message;
        
        switch (type) {
            case 'session_id':
                this.sessionId = data;
                console.log('Session ID:', data);
                break;
            case 'output':
                process.stdout.write(data);
                break;
            case 'ready':
                console.log('Terminal ready:', data);
                break;
            case 'ping':
                this.sendPong(data);
                break;
            case 'error':
                console.error('Server error:', data);
                break;
        }
    }
    
    sendCommand(command) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify({
                type: 'input',
                data: command + '\n'
            }));
        }
    }
    
    sendPong(timestamp) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify({
                type: 'pong',
                timestamp: timestamp
            }));
        }
    }
    
    resize(cols, rows) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify({
                type: 'resize',
                data: { cols, rows }
            }));
        }
    }
    
    disconnect() {
        if (this.ws) {
            this.ws.close();
        }
    }
}

// Usage example
const terminal = new TerminalClient();
terminal.connect();

// Send commands after connection
setTimeout(() => {
    terminal.sendCommand('ls -la');
    terminal.sendCommand('pwd');
    terminal.sendCommand('echo "Hello from AIO Sandbox!"');
}, 1000);

Python Asyncio Implementation

import asyncio
import websockets
import json

class AsyncTerminalClient:
    def __init__(self, base_url="ws://localhost:8080"):
        self.base_url = base_url
        self.websocket = None
        self.session_id = None
    
    async def connect(self, session_id=None):
        ws_url = f"{self.base_url}/v1/shell/ws"
        if session_id:
            ws_url += f"?session_id={session_id}"
        
        self.websocket = await websockets.connect(ws_url)
        print("Connected to terminal")
        
        # Start message handler
        asyncio.create_task(self.handle_messages())
    
    async def handle_messages(self):
        try:
            async for message in self.websocket:
                data = json.loads(message)
                await self.process_message(data)
        except websockets.exceptions.ConnectionClosed:
            print("Terminal connection closed")
        except Exception as e:
            print(f"Error handling messages: {e}")
    
    async def process_message(self, message):
        msg_type = message.get('type')
        data = message.get('data')
        
        if msg_type == 'session_id':
            self.session_id = data
            print(f"Session ID: {data}")
        elif msg_type == 'output':
            print(data, end='')
        elif msg_type == 'ready':
            print(f"Terminal ready: {data}")
        elif msg_type == 'ping':
            await self.send_pong(data)
        elif msg_type == 'error':
            print(f"Server error: {data}")
    
    async def send_command(self, command):
        if self.websocket:
            await self.websocket.send(json.dumps({
                'type': 'input',
                'data': command + '\n'
            }))
    
    async def send_pong(self, timestamp):
        if self.websocket:
            await self.websocket.send(json.dumps({
                'type': 'pong',
                'timestamp': timestamp
            }))
    
    async def resize_terminal(self, cols, rows):
        if self.websocket:
            await self.websocket.send(json.dumps({
                'type': 'resize',
                'data': {'cols': cols, 'rows': rows}
            }))
    
    async def disconnect(self):
        if self.websocket:
            await self.websocket.close()

# Usage example
async def main():
    client = AsyncTerminalClient()
    await client.connect()
    
    # Wait a bit for connection to establish
    await asyncio.sleep(1)
    
    # Send some commands
    await client.send_command('ls -la')
    await client.send_command('whoami')
    await client.send_command('python3 --version')
    
    # Keep alive for output
    await asyncio.sleep(5)
    
    await client.disconnect()

# Run the example
asyncio.run(main())

Advanced Features

Session Management and Reconnection

class AdvancedTerminalClient extends TerminalClient {
    constructor(baseUrl = 'ws://localhost:8080') {
        super(baseUrl);
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.reconnectDelay = 1000;
        this.heartbeatInterval = null;
    }
    
    connect(sessionId = null) {
        super.connect(sessionId);
        
        this.ws.on('close', (code) => {
            this.stopHeartbeat();
            if (code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
                console.log(`Connection closed (${code}). Attempting to reconnect...`);
                this.reconnect();
            }
        });
        
        this.ws.on('open', () => {
            this.reconnectAttempts = 0;
            this.startHeartbeat();
            console.log('Connected to terminal with session management');
        });
    }
    
    reconnect() {
        this.reconnectAttempts++;
        setTimeout(() => {
            console.log(`Reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
            this.connect(this.sessionId); // Reconnect to existing session
        }, this.reconnectDelay * this.reconnectAttempts);
    }
    
    startHeartbeat() {
        this.heartbeatInterval = setInterval(() => {
            if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                this.ws.send(JSON.stringify({
                    type: 'ping',
                    timestamp: Date.now()
                }));
            }
        }, 30000); // Ping every 30 seconds
    }
    
    stopHeartbeat() {
        if (this.heartbeatInterval) {
            clearInterval(this.heartbeatInterval);
            this.heartbeatInterval = null;
        }
    }
    
    async executeCommand(command, timeout = 10000) {
        return new Promise((resolve, reject) => {
            let output = '';
            let timeoutId;
            
            const originalHandler = this.handleMessage.bind(this);
            
            this.handleMessage = (message) => {
                originalHandler(message);
                
                if (message.type === 'output') {
                    output += message.data;
                    
                    // Simple command completion detection
                    if (output.includes('$') && output.trim().endsWith('$')) {
                        clearTimeout(timeoutId);
                        this.handleMessage = originalHandler;
                        resolve(output);
                    }
                }
            };
            
            timeoutId = setTimeout(() => {
                this.handleMessage = originalHandler;
                reject(new Error('Command timeout'));
            }, timeout);
            
            this.sendCommand(command);
        });
    }
}

// Usage with session persistence
const terminalWithSession = new AdvancedTerminalClient();
terminalWithSession.connect();

// Example of command execution with promise
terminalWithSession.executeCommand('ls -la').then(output => {
    console.log('Command output:', output);
}).catch(error => {
    console.error('Command failed:', error);
});

Multi-Session Management

import asyncio
import websockets
import json
from typing import Dict, Optional

class MultiSessionTerminal:
    def __init__(self, base_url="ws://localhost:8080"):
        self.base_url = base_url
        self.sessions: Dict[str, websockets.WebSocketServerProtocol] = {}
        self.session_outputs: Dict[str, str] = {}
    
    async def create_session(self, session_name: str) -> str:
        \"\"\"Create a new terminal session\"\"\"
        ws_url = f"{self.base_url}/v1/shell/ws"
        websocket = await websockets.connect(ws_url)
        
        # Wait for session ID
        session_id = None
        async for message in websocket:
            data = json.loads(message)
            if data.get('type') == 'session_id':
                session_id = data.get('data')
                break
        
        if session_id:
            self.sessions[session_name] = websocket
            self.session_outputs[session_name] = ""
            
            # Start message handler for this session
            asyncio.create_task(self.handle_session_messages(session_name))
            
            return session_id
        else:
            raise Exception("Failed to get session ID")
    
    async def connect_to_session(self, session_name: str, session_id: str):
        \"\"\"Connect to an existing session\"\"\"
        ws_url = f"{self.base_url}/v1/shell/ws?session_id={session_id}"
        websocket = await websockets.connect(ws_url)
        
        self.sessions[session_name] = websocket
        if session_name not in self.session_outputs:
            self.session_outputs[session_name] = ""
        
        asyncio.create_task(self.handle_session_messages(session_name))
    
    async def handle_session_messages(self, session_name: str):
        \"\"\"Handle messages for a specific session\"\"\"
        websocket = self.sessions[session_name]
        
        try:
            async for message in websocket:
                data = json.loads(message)
                await self.process_session_message(session_name, data)
        except websockets.exceptions.ConnectionClosed:
            print(f"Session {session_name} connection closed")
            if session_name in self.sessions:
                del self.sessions[session_name]
    
    async def process_session_message(self, session_name: str, message):
        msg_type = message.get('type')
        data = message.get('data')
        
        if msg_type == 'output':
            self.session_outputs[session_name] += data
            print(f"[{session_name}] {data}", end='')
        elif msg_type == 'ready':
            print(f"[{session_name}] Terminal ready: {data}")
        elif msg_type == 'ping':
            await self.send_pong(session_name, data)
    
    async def send_command(self, session_name: str, command: str):
        \"\"\"Send command to a specific session\"\"\"
        if session_name in self.sessions:
            websocket = self.sessions[session_name]
            await websocket.send(json.dumps({
                'type': 'input',
                'data': command + '\n'
            }))
        else:
            raise Exception(f"Session {session_name} not found")
    
    async def send_pong(self, session_name: str, timestamp):
        if session_name in self.sessions:
            websocket = self.sessions[session_name]
            await websocket.send(json.dumps({
                'type': 'pong',
                'timestamp': timestamp
            }))
    
    async def get_session_output(self, session_name: str) -> str:
        \"\"\"Get accumulated output for a session\"\"\"
        return self.session_outputs.get(session_name, "")
    
    async def close_session(self, session_name: str):
        \"\"\"Close a specific session\"\"\"
        if session_name in self.sessions:
            await self.sessions[session_name].close()
            del self.sessions[session_name]
            if session_name in self.session_outputs:
                del self.session_outputs[session_name]
    
    async def close_all_sessions(self):
        \"\"\"Close all sessions\"\"\"
        for session_name in list(self.sessions.keys()):
            await self.close_session(session_name)

# Usage example
async def multi_session_demo():
    terminal = MultiSessionTerminal()
    
    # Create multiple sessions
    session1_id = await terminal.create_session("build")
    session2_id = await terminal.create_session("test")
    session3_id = await terminal.create_session("dev")
    
    print(f"Created sessions: build={session1_id}, test={session2_id}, dev={session3_id}")
    
    # Wait for sessions to be ready
    await asyncio.sleep(2)
    
    # Run different commands in parallel
    await asyncio.gather(
        terminal.send_command("build", "npm run build"),
        terminal.send_command("test", "npm test"),
        terminal.send_command("dev", "npm run dev")
    )
    
    # Let commands run
    await asyncio.sleep(10)
    
    # Get outputs
    build_output = await terminal.get_session_output("build")
    test_output = await terminal.get_session_output("test")
    
    print("Build output length:", len(build_output))
    print("Test output length:", len(test_output))
    
    # Clean up
    await terminal.close_all_sessions()

asyncio.run(multi_session_demo())

xterm.js Frontend Integration

<!DOCTYPE html>
<html>
<head>
    <title>AIO Sandbox Terminal</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
    <style>
        #terminal { height: 400px; width: 100%; }
        .controls { margin: 10px 0; }
        button { margin: 5px; padding: 10px; }
    </style>
</head>
<body>
    <h1>AIO Sandbox Terminal Interface</h1>
    
    <div class="controls">
        <button onclick="connectTerminal()">Connect</button>
        <button onclick="disconnectTerminal()">Disconnect</button>
        <button onclick="clearTerminal()">Clear</button>
        <button onclick="resizeTerminal()">Fit</button>
    </div>
    
    <div id="terminal"></div>
    
    <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
    
    <script>
        let terminal, websocket, fitAddon;
        
        function initTerminal() {
            terminal = new Terminal({
                cursorBlink: true,
                theme: {
                    background: '#1e1e1e',
                    foreground: '#ffffff'
                }
            });
            
            fitAddon = new FitAddon.FitAddon();
            terminal.loadAddon(fitAddon);
            
            terminal.open(document.getElementById('terminal'));
            fitAddon.fit();
            
            terminal.onData(data => {
                if (websocket && websocket.readyState === WebSocket.OPEN) {
                    websocket.send(JSON.stringify({
                        type: 'input',
                        data: data
                    }));
                }
            });
            
            terminal.onResize(({ cols, rows }) => {
                if (websocket && websocket.readyState === WebSocket.OPEN) {
                    websocket.send(JSON.stringify({
                        type: 'resize',
                        data: { cols, rows }
                    }));
                }
            });
        }
        
        function connectTerminal() {
            websocket = new WebSocket('ws://localhost:8080/v1/shell/ws');
            
            websocket.onopen = () => {
                terminal.write('\\r\\n\\x1b[32mConnected to AIO Sandbox\\x1b[0m\\r\\n');
            };
            
            websocket.onmessage = (event) => {
                const message = JSON.parse(event.data);
                handleTerminalMessage(message);
            };
            
            websocket.onclose = () => {
                terminal.write('\\r\\n\\x1b[31mConnection closed\\x1b[0m\\r\\n');
            };
        }
        
        function handleTerminalMessage(message) {
            const { type, data } = message;
            
            switch (type) {
                case 'output':
                    terminal.write(data);
                    break;
                case 'ready':
                    terminal.write(`\\r\\n\\x1b[32m${data}\\x1b[0m\\r\\n`);
                    break;
                case 'session_id':
                    console.log('Session ID:', data);
                    break;
                case 'ping':
                    websocket.send(JSON.stringify({
                        type: 'pong',
                        timestamp: data
                    }));
                    break;
            }
        }
        
        function disconnectTerminal() {
            if (websocket) {
                websocket.close();
            }
        }
        
        function clearTerminal() {
            terminal.clear();
        }
        
        function resizeTerminal() {
            fitAddon.fit();
        }
        
        // Initialize on page load
        window.onload = () => {
            initTerminal();
        };
        
        // Auto-resize on window resize
        window.onresize = () => {
            if (fitAddon) {
                fitAddon.fit();
            }
        };
    </script>
</body>
</html>

React Component

import React, { useEffect, useRef, useState } from 'react';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';

const AIOTerminal = ({ baseUrl = 'ws://localhost:8080' }) => {
    const terminalRef = useRef(null);
    const [terminal, setTerminal] = useState(null);
    const [websocket, setWebsocket] = useState(null);
    const [connected, setConnected] = useState(false);
    const [sessionId, setSessionId] = useState(null);
    const fitAddonRef = useRef(null);
    
    useEffect(() => {
        // Initialize terminal
        const term = new Terminal({
            cursorBlink: true,
            theme: {
                background: '#1e1e1e',
                foreground: '#ffffff'
            }
        });
        
        const fitAddon = new FitAddon();
        term.loadAddon(fitAddon);
        fitAddonRef.current = fitAddon;
        
        term.open(terminalRef.current);
        fitAddon.fit();
        
        setTerminal(term);
        
        // Handle user input
        term.onData(data => {
            if (websocket && websocket.readyState === WebSocket.OPEN) {
                websocket.send(JSON.stringify({
                    type: 'input',
                    data: data
                }));
            }
        });
        
        // Handle terminal resize
        term.onResize(({ cols, rows }) => {
            if (websocket && websocket.readyState === WebSocket.OPEN) {
                websocket.send(JSON.stringify({
                    type: 'resize',
                    data: { cols, rows }
                }));
            }
        });
        
        return () => {
            term.dispose();
        };
    }, []);
    
    const connect = () => {
        const ws = new WebSocket(`${baseUrl}/v1/shell/ws`);
        
        ws.onopen = () => {
            setConnected(true);
            terminal.write('\\r\\n\\x1b[32mConnected to AIO Sandbox\\x1b[0m\\r\\n');
        };
        
        ws.onmessage = (event) => {
            const message = JSON.parse(event.data);
            handleMessage(message);
        };
        
        ws.onclose = () => {
            setConnected(false);
            terminal.write('\\r\\n\\x1b[31mConnection closed\\x1b[0m\\r\\n');
        };
        
        ws.onerror = (error) => {
            console.error('WebSocket error:', error);
            terminal.write('\\r\\n\\x1b[31mConnection error\\x1b[0m\\r\\n');
        };
        
        setWebsocket(ws);
    };
    
    const handleMessage = (message) => {
        const { type, data } = message;
        
        switch (type) {
            case 'session_id':
                setSessionId(data);
                break;
            case 'output':
                terminal.write(data);
                break;
            case 'ready':
                terminal.write(`\\r\\n\\x1b[32m${data}\\x1b[0m\\r\\n`);
                break;
            case 'ping':
                if (websocket) {
                    websocket.send(JSON.stringify({
                        type: 'pong',
                        timestamp: data
                    }));
                }
                break;
            case 'error':
                terminal.write(`\\r\\n\\x1b[31mError: ${data}\\x1b[0m\\r\\n`);
                break;
        }
    };
    
    const disconnect = () => {
        if (websocket) {
            websocket.close();
            setWebsocket(null);
        }
    };
    
    const clear = () => {
        if (terminal) {
            terminal.clear();
        }
    };
    
    const fit = () => {
        if (fitAddonRef.current) {
            fitAddonRef.current.fit();
        }
    };
    
    return (
        <div>
            <div style={{ marginBottom: '10px' }}>
                <button onClick={connect} disabled={connected}>
                    Connect
                </button>
                <button onClick={disconnect} disabled={!connected}>
                    Disconnect
                </button>
                <button onClick={clear}>Clear</button>
                <button onClick={fit}>Fit</button>
                <span style={{ marginLeft: '20px' }}>
                    Status: {connected ? 'Connected' : 'Disconnected'}
                    {sessionId && ` (Session: ${sessionId.substr(0, 8)}...)`}
                </span>
            </div>
            <div 
                ref={terminalRef} 
                style={{ 
                    height: '400px', 
                    border: '1px solid #ccc',
                    borderRadius: '4px'
                }}
            />
        </div>
    );
};

export default AIOTerminal;

Next Steps