Terminal-Bridge.tsxā¢21.5 kB
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Terminal as Xterm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import { useTool } from '@modelcontextprotocol/sdk/react';
import Console from './Console.js';
import { TenantToolBridge } from '../src/tenant-tool-bridge.js';
import MultiTenantOrchestrator from '../src/multi-tenant-orchestrator.js';
type Tab = 'terminal' | 'console' | 'session';
interface Session {
id: string;
status: 'active' | 'idle' | 'expired';
workspace: string;
gitConfig: {
name?: string;
email?: string;
pat?: string;
};
createdAt: string;
lastActivity: string;
}
export const TerminalBridge: React.FC<{
terminalOutput: string[];
setTerminalOutput: React.Dispatch<React.SetStateAction<string[]>>;
currentSession: Session | null;
orchestrator: MultiTenantOrchestrator;
}> = ({ terminalOutput, setTerminalOutput, currentSession, orchestrator }) => {
const termElRef = useRef<HTMLDivElement>(null);
const termRef = useRef<Xterm | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null);
const [activeTab, setActiveTab] = useState<Tab>('terminal');
const [cwd, setCwd] = useState('/workspace');
const commandHistory = useRef<string[]>([]);
const historyIndex = useRef<number>(-1);
const [gitConfig, setGitConfig] = useState({ name: '', email: '', pat: '' });
const [bridgeStatus, setBridgeStatus] = useState<any>(null);
// Get tool bridge from orchestrator
const toolBridge = orchestrator.getToolBridge();
const termWrite = (text: string) => termRef.current?.write(text.replace(/\n/g, '\r\n'));
const getPrompt = (path: string) => `\r\n\x1b[1;34mpyforge-bridge\x1b[0m:\x1b[1;32m${path}\x1b[0m$ `;
// Create new session
const handleCreateSession = useCallback(async () => {
try {
const session = await orchestrator.createSession({
name: gitConfig.name || undefined,
email: gitConfig.email || undefined,
pat: gitConfig.pat || undefined
});
termWrite(`ā
Session created: ${session.id.substring(0, 8)}...\r\n`);
termWrite(`š Workspace: ${session.workspace}\r\n`);
setCwd('/workspace');
// Update bridge status
const status = await toolBridge.getBridgeStatus();
setBridgeStatus(status);
termWrite(`š Bridge Status: ${status.status}\r\n`);
} catch (error: any) {
termWrite(`ā Error creating session: ${error.message}\r\n`);
}
}, [gitConfig, orchestrator, toolBridge]);
// Update session git config
const handleUpdateGitConfig = useCallback(async () => {
if (!currentSession) {
termWrite(`ā No active session. Create a session first.\r\n`);
return;
}
try {
const success = await orchestrator.updateSessionGitConfig(currentSession.id, {
name: gitConfig.name || undefined,
email: gitConfig.email || undefined,
pat: gitConfig.pat || undefined
});
if (success) {
termWrite(`ā
Git configuration updated for session ${currentSession.id.substring(0, 8)}...\r\n`);
} else {
termWrite(`ā Failed to update git config\r\n`);
}
} catch (error: any) {
termWrite(`ā Error updating git config: ${error.message}\r\n`);
}
}, [currentSession, gitConfig, orchestrator]);
// Show session info
const handleShowSessions = useCallback(() => {
try {
const info = orchestrator.getSessionInfo();
termWrite(`š Session Overview:\r\n`);
termWrite(` Total Sessions: ${info.totalSessions}\r\n`);
termWrite(` Active Sessions: ${info.activeSessions}\r\n\r\n`);
info.sessions.forEach((session: any) => {
termWrite(` š·ļø ${session.id} - ${session.status} (${session.age})\r\n`);
});
} catch (error: any) {
termWrite(`ā Error getting session info: ${error.message}\r\n`);
}
}, [orchestrator]);
// Test bridge connectivity
const handleTestBridge = useCallback(async () => {
try {
termWrite(`š Testing bridge connectivity...\r\n`);
const status = await toolBridge.getBridgeStatus();
setBridgeStatus(status);
termWrite(`š Bridge Status: ${status.status}\r\n`);
termWrite(`š” Server Universal: ${status.serverUniversalConnected ? 'ā
Connected' : 'ā Disconnected'}\r\n`);
if (status.error) {
termWrite(`ā Bridge Error: ${status.error}\r\n`);
}
const tools = status.availableTools || [];
termWrite(`š ļø Available Tools: ${tools.join(', ')}\r\n`);
} catch (error: any) {
termWrite(`ā Bridge test failed: ${error.message}\r\n`);
}
}, [toolBridge]);
// Execute command through bridge
const handleBridgeCommand = useCallback(async (command: string, sessionId: string) => {
try {
termWrite(`š Executing via bridge: ${command}\r\n`);
const result = await toolBridge.executeTool(sessionId, 'bash', { command });
if (result.success) {
if (result.stdout) {
termWrite(`${result.stdout}\r\n`);
}
if (result.stderr) {
termWrite(`ā ļø ${result.stderr}\r\n`);
}
} else {
termWrite(`ā Command failed: ${result.message}\r\n`);
if (result.stderr) {
termWrite(`Error output: ${result.stderr}\r\n`);
}
}
} catch (error: any) {
termWrite(`ā Bridge execution error: ${error.message}\r\n`);
}
}, [toolBridge]);
// Execute database command through bridge
const handleDatabaseCommand = useCallback(async (command: string, sessionId: string) => {
try {
termWrite(`š Searching database: ${command}\r\n`);
const result = await toolBridge.executeTool(sessionId, 'search_local', { query: command });
if (result.success && result.data) {
termWrite(`š Search Results:\r\n`);
termWrite(`${JSON.stringify(result.data, null, 2)}\r\n`);
} else {
termWrite(`ā Search failed: ${result.message}\r\n`);
}
} catch (error: any) {
termWrite(`ā Database search error: ${error.message}\r\n`);
}
}, [toolBridge]);
useEffect(() => {
if (!termElRef.current || termRef.current) return;
const term = new Xterm({
cursorBlink: true, fontFamily: 'monospace', fontSize: 13,
theme: { background: 'var(--background-primary)', foreground: 'var(--text-primary)', cursor: 'var(--accent-primary)' },
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(termElRef.current);
fitAddon.fit();
termRef.current = term;
fitAddonRef.current = fitAddon;
term.writeln("š Welcome to PyForge Bridge Terminal!");
term.writeln("š Connected to Server-Universal tools via Tenant Tool Bridge");
term.writeln("š Each user gets their own isolated workspace with git configuration.");
term.writeln("");
term.writeln("š§ Session Management Commands:");
term.writeln(" \x1b[38;5;244msession-create\x1b[0m - Create new isolated session");
term.writeln(" \x1b[38;5;244msession-info\x1b[0m - Show all active sessions");
term.writeln(" \x1b[38;5;244mgit-config-set\x1b[0m - Set git configuration for current session");
term.writeln(" \x1b[38;5;244mbridge-test\x1b[0m - Test bridge connectivity");
term.writeln("");
term.writeln("š ļø Tool Commands:");
term.writeln(" \x1b[38;5;244mbash <command>\x1b[0m - Execute bash command via bridge");
term.writeln(" \x1b[38;5;244msearch <query>\x1b[0m - Search EGW writings database");
term.writeln(" \x1b[38;5;244madmin <command>\x1b[0m - Execute admin command (requires password)");
term.writeln("");
term.writeln("š» All commands are executed within your isolated workspace.");
term.writeln("š Internet connectivity is enabled for all sessions.");
term.writeln("");
if (currentSession) {
term.writeln(`š·ļø Current Session: ${currentSession.id.substring(0, 8)}... (${currentSession.status})`);
term.writeln(`š Workspace: ${currentSession.workspace}`);
term.writeln(`š§ Git Config: ${currentSession.gitConfig.name ? 'ā
Configured' : 'ā Not configured'}`);
term.writeln("");
} else {
term.writeln("ā No active session. Use 'session-create' to start.");
term.writeln("");
}
// Show bridge status if available
if (bridgeStatus) {
term.writeln(`š Bridge Status: ${bridgeStatus.status}`);
term.writeln(`š” Server Universal: ${bridgeStatus.serverUniversalConnected ? 'ā
Connected' : 'ā Disconnected'}`);
term.writeln("");
}
term.write(getPrompt(cwd));
let currentLine = '';
const executeCommand = async (command: string) => {
if (!command.trim()) {
term.write(getPrompt(cwd));
return;
}
if (commandHistory.current[commandHistory.current.length - 1] !== command) {
commandHistory.current.push(command);
}
historyIndex.current = commandHistory.current.length;
// Handle special session commands
if (command === 'session-create') {
term.write('\r\n');
handleCreateSession();
term.write(getPrompt(cwd));
return;
}
if (command === 'session-info') {
term.write('\r\n');
handleShowSessions();
term.write(getPrompt(cwd));
return;
}
if (command === 'git-config-set') {
term.write('\r\n');
handleUpdateGitConfig();
term.write(getPrompt(cwd));
return;
}
if (command === 'bridge-test') {
term.write('\r\n');
handleTestBridge();
term.write(getPrompt(cwd));
return;
}
// Handle bridge commands
if (!currentSession) {
term.write('\r\nā No active session. Use "session-create" to start.\r\n');
term.write(getPrompt(cwd));
return;
}
// Parse command type
const parts = command.trim().split(' ');
const cmd = parts[0];
if (cmd === 'bash' && parts.length > 1) {
const bashCommand = parts.slice(1).join(' ');
term.write('\r\n');
await handleBridgeCommand(bashCommand, currentSession.id);
term.write(getPrompt(cwd));
return;
}
if (cmd === 'search' && parts.length > 1) {
const searchQuery = parts.slice(1).join(' ');
term.write('\r\n');
await handleDatabaseCommand(searchQuery, currentSession.id);
term.write(getPrompt(cwd));
return;
}
if (cmd === 'admin' && parts.length > 1) {
const adminCommand = parts.slice(1).join(' ');
term.write('\r\n');
const result = await toolBridge.executeTool(currentSession.id, 'admin_local_server', {
command: adminCommand,
adminPassword: process.env.ADMIN_PASSWORD || 'admin18401844'
});
if (result.success) {
if (result.stdout) termWrite(`${result.stdout}\r\n`);
if (result.stderr) termWrite(`ā ļø ${result.stderr}\r\n`);
} else {
termWrite(`ā Admin command failed: ${result.message}\r\n`);
}
term.write(getPrompt(cwd));
return;
}
// Default: treat as bash command
term.write('\r\n');
await handleBridgeCommand(command, currentSession.id);
term.write(getPrompt(cwd));
};
const keyListener = term.onKey(({ key, domEvent }) => {
const printable = !domEvent.altKey && !domEvent.ctrlKey && !domEvent.metaKey;
if (domEvent.key === 'Enter') {
term.write('\r\n');
executeCommand(currentLine);
currentLine = '';
} else if (domEvent.key === 'Backspace') {
if (currentLine.length > 0) {
term.write('\b \b');
currentLine = currentLine.slice(0, -1);
}
} else if (domEvent.key === 'ArrowUp') {
if(historyIndex.current > 0) {
historyIndex.current--;
const cmd = commandHistory.current[historyIndex.current];
term.write(`\x1b[2K\r${getPrompt(cwd).trim()}${cmd}`);
currentLine = cmd;
}
} else if (domEvent.key === 'ArrowDown') {
if(historyIndex.current < commandHistory.current.length - 1) {
historyIndex.current++;
const cmd = commandHistory.current[historyIndex.current];
term.write(`\x1b[2K\r${getPrompt(cwd).trim()}${cmd}`);
currentLine = cmd;
} else {
historyIndex.current = commandHistory.current.length;
term.write(`\x1b[2K\r${getPrompt(cwd).trim()}`);
currentLine = '';
}
} else if (printable) {
currentLine += key;
term.write(key);
}
});
const resizeObserver = new ResizeObserver(() => fitAddon.fit());
resizeObserver.observe(termElRef.current);
return () => {
keyListener.dispose();
resizeObserver.disconnect();
};
}, [activeTab, cwd, handleCreateSession, handleShowSessions, handleUpdateGitConfig, handleTestBridge, handleBridgeCommand, handleDatabaseCommand, currentSession, toolBridge, bridgeStatus]);
useEffect(() => {
if (activeTab === 'terminal') {
setTimeout(() => {
fitAddonRef.current?.fit();
termRef.current?.focus();
}, 10);
}
}, [activeTab]);
const TabButton: React.FC<{ name: string; tabId: Tab; }> = ({ name, tabId }) => (
<button
onClick={() => setActiveTab(tabId)}
style={{...styles.tabButton, ...(activeTab === tabId ? styles.activeTabButton : {})}}
>{name}</button>
);
return (
<div style={styles.container}>
<div style={styles.tabHeader}>
<TabButton name="Terminal" tabId="terminal" />
<TabButton name="Console" tabId="console" />
<TabButton name="Session" tabId="session" />
</div>
<div style={styles.tabContent}>
<div style={{...styles.tabPanel, display: activeTab === 'terminal' ? 'block' : 'none' }}>
<div ref={termElRef} style={{width: '100%', height: '100%'}} />
</div>
<div style={{...styles.tabPanel, display: activeTab === 'console' ? 'block' : 'none' }}>
<Console output={terminalOutput} onClear={() => setTerminalOutput(['Console cleared.'])} />
</div>
<div style={{...styles.tabPanel, display: activeTab === 'session' ? 'block' : 'none' }}>
<div style={styles.sessionPanel}>
<h3>Session Configuration</h3>
<div style={styles.currentSessionInfo}>
{currentSession ? (
<div>
<p><strong>Current Session:</strong> {currentSession.id.substring(0, 8)}...</p>
<p><strong>Status:</strong> {currentSession.status}</p>
<p><strong>Workspace:</strong> {currentSession.workspace}</p>
<p><strong>Git Configured:</strong> {currentSession.gitConfig.name ? 'ā
Yes' : 'ā No'}</p>
<p><strong>Bridge Status:</strong> {bridgeStatus?.status || 'Unknown'}</p>
</div>
) : (
<p><strong>No active session</strong> - Create one to start working</p>
)}
</div>
<div style={styles.configSection}>
<h4>Git Configuration</h4>
<input
type="text"
placeholder="Git User Name"
value={gitConfig.name}
onChange={(e) => setGitConfig(prev => ({ ...prev, name: e.target.value }))}
style={styles.input}
/>
<input
type="email"
placeholder="Git User Email"
value={gitConfig.email}
onChange={(e) => setGitConfig(prev => ({ ...prev, email: e.target.value }))}
style={styles.input}
/>
<input
type="password"
placeholder="GitHub Personal Access Token"
value={gitConfig.pat}
onChange={(e) => setGitConfig(prev => ({ ...prev, pat: e.target.value }))}
style={styles.input}
/>
<div style={styles.buttonGroup}>
<button onClick={handleCreateSession} style={styles.button}>
Create New Session
</button>
<button onClick={handleUpdateGitConfig} style={styles.button} disabled={!currentSession}>
Update Git Config
</button>
<button onClick={handleShowSessions} style={styles.button}>
Show All Sessions
</button>
<button onClick={handleTestBridge} style={styles.button}>
Test Bridge
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
container: { height: '100%', width: '100%', display: 'flex', flexDirection: 'column', backgroundColor: 'var(--background-secondary)' },
tabHeader: { display: 'flex', flexShrink: 0 },
tabButton: { background: 'var(--background-tertiary)', color: 'var(--text-secondary)', border: 'none', padding: '8px 12px', cursor: 'pointer', borderBottom: '1px solid var(--border-color)' },
activeTabButton: { background: 'var(--background-primary)', color: 'var(--text-primary)', borderBottom: '1px solid transparent' },
tabContent: { flex: 1, overflow: 'hidden', backgroundColor: 'var(--background-primary)' },
tabPanel: { width: '100%', height: '100%', padding: '5px' },
sessionPanel: { padding: '20px', height: '100%', overflow: 'auto' },
currentSessionInfo: { marginBottom: '20px', padding: '15px', backgroundColor: 'var(--background-secondary)', borderRadius: '8px' },
configSection: { marginBottom: '20px' },
input: {
width: '100%',
padding: '10px',
margin: '5px 0',
border: '1px solid var(--border-color)',
borderRadius: '4px',
backgroundColor: 'var(--background-primary)',
color: 'var(--text-primary)'
},
buttonGroup: { display: 'flex', gap: '10px', marginTop: '15px', flexWrap: 'wrap' },
button: {
padding: '10px 15px',
backgroundColor: 'var(--accent-primary)',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}
};
export default TerminalBridge;