Interactive Web Terminal
Connect your secure SSH server to our platform so your audience can view and copy your terminal commands in real time.
Why Use Our Web Terminal?
Instead of demonstrating tasks on an isolated, local computer screen, our Web Terminal enables unparalleled presentation interaction. Our secure WebSocket infrastructure captures and syncs commands and outputs natively to every audience member's web browser, allowing individuals to cleanly view, highlight, and copy terminal text alongside your stream.
1. Configuration & Security Tokens
Before launching a broadcast session, configure the link on your account profiles:
- Navigate to your User Profile Page and switch on the Web Terminal Feature toggles.
- Provide your persistent Web Terminal Username (the system account name for your hosting SSH service). Saving this credential speeds up your execution flow so you won't need to manually retype names during live events.
- The dashboard will generate a unique signature string. Copy this secure cryptographic TOKEN to authorize your deployment daemon.
2. Linux SSH Server Environment Setup & Deployment
To run the relay agent on your remote Linux SSH server, you need to set up a quick Node.js environment, install the required packages, and execute the daemon script in the background.
1. Initialize Project
Create a dedicated folder and install the required WebSocket and SSH2 dependencies via npm:
mkdir pa-agent && cd pa-agent
npm init -y
npm install ws ssh2
2. Create Script
Create a file named agent.js, paste the code below, and replace the placeholder variables with your credentials:
- Replace
YOUR_GENERATED_PROFILE_TOKEN_HEREwith your account token. - Replace
YOUR_PA_USER_NAMEwith your Presentation Assistants username.
3. Run in Background
To prevent the agent from closing when you disconnect your SSH session, launch it with nohup or use pm2:
# Option A: Using nohup
nohup node agent.js > agent.log 2>&1 &
# Option B: Using PM2 (Recommended)
npm install -g pm2
pm2 start agent.js --name "pa-agent"
The Relay Script (agent.js):
// agent.js
const WebSocket = require('ws');
const { Client } = require('ssh2');
const SERVER_URL = 'wss://ws.presentationassistants.com:3004';
const TOKEN = 'YOUR_TOKEN';
const USERNAME = 'YOUR_USERNAME';
let ws = null;
const sessions = new Map();
let reconnectTimeout = 1000;
function connect() {
ws = new WebSocket(SERVER_URL);
ws.on('open', () => {
console.log('[WS] Connected to server');
reconnectTimeout = 1000;
ws.send(JSON.stringify({ type: 'register', role: 'agent', username: USERNAME, token: TOKEN }));
});
ws.on('message', (msg) => {
let message;
try { message = JSON.parse(msg); } catch { return; }
const { type, sessionId, sshUsername, sshPassword, data } = message;
if (type === 'ping') {
ws.send(JSON.stringify({ type: 'pong' }));
return;
}
if (type === 'start_session') {
if (sessions.has(sessionId)) return;
const conn = new Client();
const session = { conn, stream: null };
sessions.set(sessionId, session);
conn.on('ready', () => {
if (sessions.get(sessionId) !== session) { conn.end(); return; }
console.log(`[SSH] Ready for session ${sessionId}`);
const pty = { term: 'xterm-256color',
cols: Number.isInteger(message.cols) ? message.cols : 80,
rows: Number.isInteger(message.rows) ? message.rows : 24
};
const onStream = (err, stream) => {
if (sessions.get(sessionId) !== session) { stream?.end(); return; }
if (err) {
console.error('[SSH] Shell error:', err.message);
ws.send(JSON.stringify({ type: 'session_error', sessionId, error: err.message }));
cleanupSession(sessionId);
return;
}
console.log(`[SSH] Shell stream created for session ${sessionId}`);
session.stream = stream;
if (session.size) stream.setWindow(session.size.rows, session.size.cols, 0, 0);
const forwardOutput = (chunk) => {
if (sessions.get(sessionId) !== session) return;
console.log(`[SSH] Sending data to relay, session ${sessionId}, length ${chunk.length}`);
ws.send(JSON.stringify({ type: 'tcp_data', role: 'agent', sessionId, data: Buffer.from(chunk).toString('base64') }));
};
stream.on('data', forwardOutput);
stream.stderr?.on('data', forwardOutput);
stream.on('close', () => {
if (sessions.get(sessionId) !== session) return;
console.log(`[SSH] Shell closed for session ${sessionId}`);
ws.send(JSON.stringify({ type: 'session_closed', sessionId }));
cleanupSession(sessionId);
});
};
// Initialize capture as the PTY's process, before any user input. Never
// inject a launcher into an already-running shell/editor on reconnect.
if (typeof message.startupCommand === 'string' && message.startupCommand.length > 0 && message.startupCommand.length <= 65536) {
conn.exec(message.startupCommand, { pty }, onStream);
} else {
conn.shell(pty, onStream); // Compatibility with older presenter pages.
}
});
conn.on('error', (err) => {
if (sessions.get(sessionId) !== session) return;
ws.send(JSON.stringify({ type: 'session_error', sessionId, error: err.message }));
cleanupSession(sessionId);
});
conn.on('end', () => {
if (sessions.get(sessionId) === session) cleanupSession(sessionId);
});
conn.connect({
host: '127.0.0.1',
port: 22,
username: sshUsername,
password: sshPassword,
readyTimeout: 10000,
});
return;
}
if (type === 'tcp_data') {
const s = sessions.get(sessionId);
if (s?.stream) s.stream.write(Buffer.from(data, 'base64'));
return;
}
// Add this alongside your other if(type === ...) statements:
if (type === 'resize') {
const s = sessions.get(sessionId);
if (s) s.size = { rows: message.rows, cols: message.cols };
if (s && s.stream) {
// message.cols and message.rows come from your browser's fitAddon
s.stream.setWindow(message.rows, message.cols, 0, 0);
console.log(`[SSH] Resized session ${sessionId} to ${message.cols}x${message.rows}`);
}
return;
}
if (type === 'close_session') {
cleanupSession(sessionId);
return;
}
});
ws.on('close', () => {
console.log('[WS] Disconnected, reconnecting...');
attemptReconnect();
});
ws.on('error', (err) => console.error('[WS] Error:', err.message));
}
function cleanupSession(sessionId) {
const s = sessions.get(sessionId);
if (!s) return;
sessions.delete(sessionId);
try { s.stream?.end(); } catch {}
try { s.conn?.end(); } catch {}
}
function attemptReconnect() {
setTimeout(() => {
reconnectTimeout = Math.min(reconnectTimeout * 2, 30000);
connect();
}, reconnectTimeout);
}
connect();
nohup, execute tail -f agent.log. If you are tracking the status using pm2, run pm2 logs pa-agent. You should cleanly see a [WS] Connected to server message printed to standard output.
3. Presenter Runtime Execution
Once your background script confirms an established connection to our secure relays, jumping into live operations is seamless:
Menu Selection
During your presentation workspace, locate and select the Web Terminal navigation item from the active control toolbar menu layout.
Instant Authentications
Input only your host system’s SSH Password. Because your target workspace username is verified on profile configurations, authentication is instantaneous.
4. Transparent Data Multiplexing & Relay Outputs
For the Presenter
Manage code executions easily in window views. Built-in window layout resizing actions pass precise row-and-column dimensions downstream via the socket stream automatically.
For the Audience
Audience view windows sync text environments perfectly. No manual scrolling configurations are required to duplicate presenter inputs—enabling clean copy-paste buffers instantly.