shell-mirror
Version:
Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.
1,099 lines (973 loc) • 48.1 kB
HTML
<html>
<head>
<title>Terminal Mirror</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style>
html, body {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
overflow: hidden;
background-color: #000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
#auth-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
#login-btn {
padding: 15px 30px;
font-size: 18px;
color: white;
background-color: #4285F4;
border: none;
border-radius: 5px;
cursor: pointer;
}
#app-container {
display: none; /* Initially hidden */
flex-direction: column;
width: 100%;
height: 100%;
}
#terminal-container {
flex-grow: 1;
width: 100%;
overflow: hidden;
position: relative;
}
#connection-status {
position: absolute;
top: 10px;
right: 10px;
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
z-index: 1000;
}
.status-connected {
background-color: #4CAF50;
color: white;
}
.status-connecting {
background-color: #FF9800;
color: white;
}
.status-disconnected {
background-color: #f44336;
color: white;
}
.status-agent-offline {
background-color: #ff9800;
color: white;
}
.status-error {
background-color: #9C27B0;
color: white;
}
#keyboard-bar {
flex-shrink: 0;
width: 100%;
background-color: #222;
display: flex;
justify-content: space-around;
padding: 8px 0;
box-sizing: border-box;
}
.key-btn {
color: white;
background-color: #444;
border: 1px solid #666;
border-radius: 5px;
font-size: 16px;
padding: 10px 12px;
margin: 0 2px;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
</style>
</head>
<body>
<!-- Client-side logging -->
<script src="/js/client-logger.js"></script>
<div id="auth-container">
<button id="login-btn">Login with Google</button>
</div>
<div id="app-container">
<div id="terminal-container">
<div id="connection-status" class="status-connecting">Connecting...</div>
</div>
<div id="keyboard-bar">
<button class="key-btn" data-key-name="ESC">Esc</button>
<button class="key-btn" data-key-name="TAB">Tab</button>
<button class="key-btn" data-key-name="ARROW_UP">▲</button>
<button class="key-btn" data-key-name="ARROW_DOWN">▼</button>
<button class="key-btn" data-key-name="ARROW_LEFT">◀</button>
<button class="key-btn" data-key-name="ARROW_RIGHT">▶</button>
<button class="key-btn" data-key-name="ENTER">Enter</button>
</div>
</div>
<!-- Dynamically load xterm scripts only after authentication -->
<script>
const authContainer = document.getElementById('auth-container');
const appContainer = document.getElementById('app-container');
const loginBtn = document.getElementById('login-btn');
loginBtn.addEventListener('click', () => {
window.location.href = '/php-backend/api/auth-login.php';
});
// Check authentication status on page load
fetch('/php-backend/api/auth-status.php', {
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(res => {
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
return res.json();
})
.then(data => {
console.log('Auth status response:', data);
if (data.success && data.data && data.data.authenticated) {
console.log('✅ User is authenticated, loading terminal...');
authContainer.style.display = 'none';
appContainer.style.display = 'flex';
console.log('📦 Loading xterm scripts...');
loadXtermScripts(loadTerminal);
} else {
console.log('❌ User not authenticated, showing login form');
}
})
.catch(error => {
console.error('Auth status check failed:', error);
console.log('Falling back to login form due to auth check failure');
});
function loadXtermScripts(callback) {
console.log('🔄 Starting to load xterm CSS...');
const xtermCss = document.createElement('link');
xtermCss.rel = 'stylesheet';
xtermCss.href = 'https://unpkg.com/xterm@5.3.0/css/xterm.css';
document.head.appendChild(xtermCss);
console.log('🔄 Starting to load xterm JS...');
const xtermJs = document.createElement('script');
xtermJs.src = 'https://unpkg.com/xterm@5.3.0/lib/xterm.js';
document.head.appendChild(xtermJs);
xtermJs.onload = () => {
console.log('✅ Xterm JS loaded, loading fit addon...');
const fitAddonJs = document.createElement('script');
fitAddonJs.src = 'https://unpkg.com/@xterm/addon-fit@0.10.0/lib/addon-fit.js';
document.head.appendChild(fitAddonJs);
fitAddonJs.onload = () => {
console.log('✅ Fit addon loaded, initializing terminal...');
callback();
};
fitAddonJs.onerror = (error) => {
console.error('❌ Failed to load fit addon:', error);
};
};
xtermJs.onerror = (error) => {
console.error('❌ Failed to load xterm JS:', error);
};
}
function loadTerminal() {
console.log('🚀 Initializing terminal...');
const termContainer = document.getElementById('terminal-container');
const connectionStatus = document.getElementById('connection-status');
if (!termContainer) {
console.error('❌ Terminal container not found!');
return;
}
if (!connectionStatus) {
console.error('❌ Connection status element not found!');
return;
}
console.log('📦 Creating Terminal instance...');
const term = new Terminal({
cursorBlink: true,
convertEol: true,
fontSize: 14,
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(termContainer);
const keyMap = {
ARROW_UP: "\u001b[A",
ARROW_DOWN: "\u001b[B",
ARROW_RIGHT: "\u001b[C",
ARROW_LEFT: "\u001b[D",
TAB: "\u0009",
ESC: "\u001b",
ENTER: "\r"
};
let sessionId = 'terminal_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
let currentCommand = '';
let commandHistory = [];
let historyIndex = -1;
let tabCompletions = [];
let tabCompletionIndex = -1;
let lastTabPrefix = '';
// Performance enhancement variables
let commandCache = new Map();
let directoryCache = new Map();
let currentWorkingDir = '~';
let currentPrompt = '$ '; // Store the actual shell prompt
let isExecuting = false;
let executionQueue = [];
let pendingInput = ''; // Buffer input during execution
function updateConnectionStatus(status, message) {
connectionStatus.className = `status-${status}`;
connectionStatus.textContent = message;
}
function initializeTerminal() {
console.log('🚀 Initializing terminal...');
clientLogger.log('INFO', 'Terminal initialization started', {
userAgent: navigator.userAgent,
viewport: `${window.innerWidth}x${window.innerHeight}`,
timestamp: new Date().toISOString()
});
updateConnectionStatus('connecting', 'Checking connection...');
// Load cached state
loadCachedState();
// Show welcome message
term.write('\x1b[32mTerminal Mirror - Ready\x1b[0m\r\n');
term.write('Checking for Mac connection...\r\n\r\n');
// Check for real Mac agent connection
checkMacAgents().then(result => {
clientLogger.log('INFO', 'Mac agent check completed', {
hasMacAgents: result.hasMacAgents,
agentName: result.agentName,
error: result.error
});
if (result.hasMacAgents) {
updateConnectionStatus('connected', `Connected to ${result.agentName}`);
term.write(`\x1b[32m✅ Connected to Mac: ${result.agentName}\x1b[0m\r\n`);
term.write('Loading terminal environment...\r\n\r\n');
// Get initial prompt from Mac agent
executeServerCommand('pwd', true);
} else {
updateConnectionStatus('disconnected', 'No Mac connected');
term.write('\x1b[31m❌ No Mac connected to your account.\x1b[0m\r\n\r\n');
if (result.error) {
term.write(`\x1b[33m⚠️ Debug info: ${result.error}\x1b[0m\r\n\r\n`);
clientLogger.log('ERROR', 'Mac agent check failed', { error: result.error });
}
term.write('To connect your Mac:\r\n');
term.write('1. Install: npm install -g shell-mirror\r\n');
term.write('2. Run: shell-mirror\r\n');
term.write('3. Follow setup instructions\r\n\r\n');
term.write('🔍 Press Ctrl+Shift+L to view debug logs\r\n\r\n');
}
showPrompt();
}).catch(error => {
console.error('Connection check failed:', error);
updateConnectionStatus('error', 'Connection check failed');
term.write('\x1b[31m❌ Failed to check Mac connection\x1b[0m\r\n\r\n');
showPrompt();
});
setTimeout(() => {
fitAndResize();
term.focus();
console.log('✅ Terminal focused and ready');
// DISABLED: Background caching disabled to prevent stale results
// startBackgroundCaching();
}, 100);
}
function loadCachedState() {
// Restore working directory
const savedCwd = localStorage.getItem('terminal_cwd');
if (savedCwd) {
currentWorkingDir = savedCwd;
}
// Restore command history
const savedHistory = localStorage.getItem('terminal_history');
if (savedHistory) {
try {
commandHistory = JSON.parse(savedHistory);
historyIndex = commandHistory.length;
} catch (e) {
console.warn('Failed to restore command history');
}
}
// Store user info for instant whoami
if (window.userInfo) {
localStorage.setItem('terminal_user_info', JSON.stringify(window.userInfo));
}
}
function startBackgroundCaching() {
// Pre-cache current directory listing
preCacheDirectoryContents();
// Cache common system info
setTimeout(() => {
['whoami', 'uname -a'].forEach(cmd => {
if (!commandCache.has(`${currentWorkingDir}:${cmd}`)) {
executeServerCommand(cmd, true);
}
});
}, 500);
// DISABLED: Periodic cache refresh disabled
// setInterval(() => {
// refreshStaleCache();
// }, 60000); // Every minute
// Start connection monitoring
startConnectionMonitoring();
// Start periodic directory sync
startDirectorySync();
}
function refreshStaleCache() {
const now = Date.now();
const staleThreshold = 60000; // 1 minute (reduced from 5 minutes)
// Only refresh directory listings for tab completion
for (const [key, entry] of commandCache.entries()) {
if (now - entry.timestamp > staleThreshold) {
const [dir, command] = key.split(':');
if (dir === currentWorkingDir && command.startsWith('ls')) {
executeServerCommand(command, true);
}
}
}
}
function invalidateDirectoryCache(directory) {
// Remove all cache entries for the specified directory
const keysToDelete = [];
for (const [key, entry] of commandCache.entries()) {
const [dir, command] = key.split(':');
if (dir === directory) {
keysToDelete.push(key);
}
}
keysToDelete.forEach(key => {
commandCache.delete(key);
console.log('🗑️ Invalidated cache entry:', key);
});
// Also clear directory-specific cache
if (directoryCache.has(directory)) {
directoryCache.delete(directory);
console.log('🗑️ Invalidated directory cache for:', directory);
}
}
let connectionMonitorInterval;
let directorySyncInterval;
let lastConnectionCheck = Date.now();
let consecutiveFailures = 0;
function startConnectionMonitoring() {
console.log('🔍 Starting connection monitoring...');
connectionMonitorInterval = setInterval(() => {
validateConnection();
}, 30000); // Check every 30 seconds
}
function validateConnection() {
const now = Date.now();
// Check both API and Mac agent availability
fetch('/php-backend/api/agents-list.php', {
method: 'GET',
credentials: 'include',
timeout: 5000
})
.then(response => response.json())
.then(data => {
if (data.success && data.data.agents && data.data.agents.length > 0) {
const onlineAgents = data.data.agents.filter(agent => agent.onlineStatus === 'online');
if (onlineAgents.length > 0) {
// Mac agents are online
consecutiveFailures = 0;
lastConnectionCheck = now;
updateConnectionStatus('connected', 'Connected to Mac');
console.log('✅ Mac agent connection verified');
} else {
// No online Mac agents
updateConnectionStatus('agent-offline', 'Mac Agent Offline');
console.log('📱 Authenticated but no Mac agents online');
}
} else {
// No Mac agents registered
updateConnectionStatus('agent-offline', 'No Mac Agent Found');
console.log('📱 Authenticated but no Mac agents registered');
}
})
.catch(error => {
console.warn('🚨 Connection check failed:', error);
handleConnectionFailure();
});
}
function handleConnectionFailure() {
consecutiveFailures++;
console.log(`🚨 Connection failure #${consecutiveFailures}`);
if (consecutiveFailures >= 2) {
updateConnectionStatus('disconnected', 'Connection Lost - Attempting Reconnect...');
// Attempt to refresh the page after 3 consecutive failures
if (consecutiveFailures >= 3) {
console.log('🔄 Multiple connection failures, refreshing page...');
setTimeout(() => {
window.location.reload();
}, 5000);
}
}
}
function startDirectorySync() {
console.log('📍 Starting directory sync monitoring...');
// Sync directory every 2 minutes to catch any mismatches
directorySyncInterval = setInterval(() => {
executeServerCommand('pwd', true);
}, 120000); // Every 2 minutes
}
function saveState() {
// Save command history
localStorage.setItem('terminal_history', JSON.stringify(commandHistory.slice(-100))); // Keep last 100 commands
// Save working directory
localStorage.setItem('terminal_cwd', currentWorkingDir);
}
function checkMacAgents() {
console.log('🔍 Checking for Mac agents...');
return fetch('/php-backend/api/agents-list.php', {
credentials: 'include' // Include session cookies
})
.then(response => {
console.log(` Response status: ${response.status}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log(' Response data:', data);
if (data.success && data.data.agents && data.data.agents.length > 0) {
console.log(` Found ${data.data.agents.length} agents`);
const onlineAgents = data.data.agents.filter(agent => agent.onlineStatus === 'online');
console.log(` ${onlineAgents.length} agents are online`);
if (onlineAgents.length > 0) {
const agent = onlineAgents[0];
console.log(` Using agent: ${agent.machineName || agent.agentId}`);
return {
hasMacAgents: true,
agentName: agent.machineName || agent.agentId,
ownerName: agent.ownerEmail,
agentId: agent.agentId
};
}
}
console.log(' No online agents found');
return {
hasMacAgents: false,
agentName: null,
ownerName: null
};
})
.catch(error => {
console.error('❌ Failed to check Mac agents:', error);
return {
hasMacAgents: false,
agentName: null,
ownerName: null,
error: error.message
};
});
}
function showPrompt() {
// Show actual shell prompt instead of simplified version
if (currentPrompt && currentPrompt.trim() && !currentPrompt.includes('$ ')) {
// Use the actual prompt from the shell if it contains directory info
term.write(currentPrompt + ' ');
} else {
// Always show current directory in prompt
let shortDir = currentWorkingDir || '~';
// Simplify directory display for very long paths
try {
if (shortDir.length > 40) {
const parts = shortDir.split('/');
if (parts.length > 2) {
shortDir = '.../' + parts.slice(-2).join('/');
}
}
} catch (e) {
shortDir = '~';
}
term.write('\x1b[36m' + shortDir + ' $\x1b[0m ');
}
}
function executeCommand(command) {
const trimmedCommand = command.trim();
if (!trimmedCommand) {
term.write('\r\n');
showPrompt();
return;
}
// Log the exact command being executed
console.log('🎯 Executing exact command:', JSON.stringify(trimmedCommand));
// Add to history immediately
commandHistory.push(trimmedCommand);
historyIndex = commandHistory.length;
// Save state for persistence
saveState();
// Set executing state to prevent input bleeding
isExecuting = true;
pendingInput = '';
// Move to new line (preserve the typed command line)
term.write('\r\n');
// Check for instant commands first
const instantResult = executeInstantCommand(trimmedCommand);
if (instantResult !== null) {
if (instantResult) {
term.write(instantResult);
}
term.write('\r\n');
isExecuting = false; // Re-enable input
showPrompt();
processPendingInput();
return;
}
// DISABLED: Aggressive caching was causing stale results
// Only cache directory listings for tab completion, not command output
// Cache invalidation was unreliable when working directory changed
// Show execution status without cluttering terminal
updateConnectionStatus('connecting', 'Executing...');
// Execute on server
executeServerCommand(trimmedCommand, false);
}
function executeInstantCommand(command) {
const cmd = command.trim().toLowerCase();
const now = new Date();
switch (cmd) {
case 'pwd':
return currentWorkingDir;
case 'date':
return now.toString();
case 'whoami':
// Return cached user info if available
const userInfo = localStorage.getItem('terminal_user_info');
if (userInfo) {
return JSON.parse(userInfo).username || 'user';
}
break;
case 'clear':
term.clear();
return '';
case 'history':
return commandHistory.map((cmd, i) => `${i + 1} ${cmd}`).join('\n');
}
return null; // Not an instant command
}
function executeServerCommand(command, isBackgroundRefresh = false) {
const startTime = Date.now();
fetch('/php-backend/api/terminal-execute.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: command,
session_id: sessionId
})
})
.then(response => response.json())
.then(data => {
const responseTime = Date.now() - startTime;
if (!isBackgroundRefresh) {
// Check if command was executed by Mac agent or returned error
if (data.success && data.data.mac_agent) {
updateConnectionStatus('connected', 'Connected to Mac');
} else if (data.success && !data.data.mac_agent) {
updateConnectionStatus('agent-offline', 'Mac Agent Offline');
} else {
updateConnectionStatus('error', 'Command Failed');
}
}
if (data.success) {
const output = data.data.output || '';
// DISABLED: All caching disabled to prevent stale results
// No caching of any command results
// Update working directory if cd command
updateWorkingDirectory(command, output);
// Update prompt from Mac agent response
if (data.data.prompt) {
currentPrompt = data.data.prompt;
console.log('🎯 Updated prompt from Mac agent:', currentPrompt);
}
// DISABLED: Pre-caching disabled to prevent stale results
// if (command.startsWith('cd ')) {
// preCacheDirectoryContents();
// }
if (!isBackgroundRefresh) {
if (output) {
const formattedOutput = output.replace(/\n/g, '\r\n');
term.write(formattedOutput);
// Only add newline if output doesn't already end with one
if (!output.endsWith('\n')) {
term.write('\r\n');
}
}
isExecuting = false; // Re-enable input
showPrompt();
processPendingInput();
}
} else {
if (!isBackgroundRefresh) {
// Provide clearer error messages based on the failure type
let errorMessage = data.message || 'Command execution failed';
if (errorMessage.includes('No Mac connected') || errorMessage.includes('No agents available')) {
updateConnectionStatus('agent-offline', 'Mac Agent Required');
term.write('\x1b[33m📱 No Mac agent connected. Please start the Mac agent on your computer.\x1b[0m\r\n');
term.write('\x1b[90mRun: shell-mirror start\x1b[0m\r\n');
} else {
updateConnectionStatus('error', 'Command Failed');
term.write('\x1b[31m❌ Error: ' + errorMessage + '\x1b[0m\r\n');
}
isExecuting = false; // Re-enable input
showPrompt();
processPendingInput();
}
}
})
.catch(error => {
if (!isBackgroundRefresh) {
updateConnectionStatus('disconnected', 'Connection Lost');
console.error('Command execution failed:', error);
term.write('\x1b[31m🚫 Lost connection to server. Checking connectivity...\x1b[0m\r\n');
// Trigger immediate connection validation
setTimeout(() => {
validateConnection();
}, 1000);
isExecuting = false; // Re-enable input
showPrompt();
processPendingInput();
}
});
}
function updateWorkingDirectory(command, output) {
if (command.startsWith('cd ')) {
// Multiple methods to detect successful directory change
let newDir = null;
// Method 1: Look for "Changed directory to:" message
if (output && output.includes('Changed directory to:')) {
const match = output.match(/Changed directory to:\s*(.+)/);
if (match) {
newDir = match[1].trim();
}
}
// Method 2: If no error message, assume success for simple cases
if (!newDir && output && !output.includes('no such file') && !output.includes('Not a directory') && !output.includes('Permission denied')) {
const target = command.substring(3).trim();
if (target === '' || target === '~') {
newDir = '~';
}
// Don't try to construct relative paths - will verify with pwd
}
if (newDir) {
const oldDir = currentWorkingDir;
currentWorkingDir = newDir;
localStorage.setItem('terminal_cwd', currentWorkingDir);
console.log('🗂️ Updated working directory to:', currentWorkingDir);
// Invalidate old directory cache entries
invalidateDirectoryCache(oldDir);
// Verify with pwd in background to ensure sync
setTimeout(() => {
executeServerCommand('pwd', true);
}, 100);
return;
}
// Check if command failed - do NOT update directory
if (output && (output.includes('no such file or directory') ||
output.includes('Not a directory') ||
output.includes('Permission denied') ||
output.includes('cd:'))) {
console.log('❌ cd command failed, keeping current directory:', currentWorkingDir);
return; // Don't update directory on failure
}
// Only fallback to manual construction if no output (shouldn't happen with Mac agent)
console.warn('⚠️ No output from cd command, using fallback logic');
const target = command.substring(3).trim();
if (target === '' || target === '~') {
const oldDir = currentWorkingDir;
currentWorkingDir = '~';
localStorage.setItem('terminal_cwd', currentWorkingDir);
console.log('🗂️ Updated working directory to home:', currentWorkingDir);
invalidateDirectoryCache(oldDir);
}
// Don't attempt relative path construction without confirmation
} else if (command === 'pwd' && output) {
const oldDir = currentWorkingDir;
const newDir = output.trim();
// Only update if we got a valid directory path
if (newDir && newDir.startsWith('/') || newDir === '~') {
currentWorkingDir = newDir;
localStorage.setItem('terminal_cwd', currentWorkingDir);
console.log('🗂️ Updated working directory from pwd:', currentWorkingDir);
if (oldDir !== currentWorkingDir) {
invalidateDirectoryCache(oldDir);
console.log('📍 Directory sync: was', oldDir, 'now', currentWorkingDir);
}
}
}
}
function preCacheDirectoryContents() {
// Pre-cache ls results for current directory
setTimeout(() => {
['ls', 'ls -la', 'ls -l'].forEach(lsCmd => {
const cacheKey = `${currentWorkingDir}:${lsCmd}`;
if (!commandCache.has(cacheKey)) {
executeServerCommand(lsCmd, true);
}
});
}, 200);
}
function handleSpecialKeys(sequence) {
switch (sequence) {
case keyMap.ARROW_UP:
// History up
if (historyIndex > 0) {
historyIndex--;
replaceCurrentCommand(commandHistory[historyIndex] || '');
resetTabCompletion();
}
break;
case keyMap.ARROW_DOWN:
// History down
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
replaceCurrentCommand(commandHistory[historyIndex] || '');
} else {
historyIndex = commandHistory.length;
replaceCurrentCommand('');
}
resetTabCompletion();
break;
case keyMap.ENTER:
executeCommand(currentCommand);
currentCommand = '';
break;
case keyMap.TAB:
handleTabCompletion();
break;
default:
return false; // Not handled
}
return true; // Handled
}
function replaceCurrentCommand(newCommand) {
// Clear current line
term.write('\x1b[2K\r'); // Clear line and return to start
showPrompt();
term.write(newCommand);
currentCommand = newCommand;
console.log('🔄 Command replaced with:', JSON.stringify(newCommand));
}
function handleTabCompletion() {
// Reset tab completion if command changed
const words = currentCommand.split(' ');
const currentWord = words[words.length - 1] || '';
if (lastTabPrefix !== currentWord) {
tabCompletions = [];
tabCompletionIndex = -1;
lastTabPrefix = currentWord;
}
if (tabCompletions.length === 0) {
// First tab - get completions from server
getTabCompletions(currentWord).then(completions => {
if (completions.length === 0) {
// No completions available
return;
} else if (completions.length === 1) {
// Single completion - auto-complete immediately
completeCurrentWord(completions[0]);
} else {
// Multiple completions - start cycling
tabCompletions = completions;
tabCompletionIndex = 0;
completeCurrentWord(completions[0]);
}
});
} else {
// Subsequent tabs - cycle through completions
tabCompletionIndex = (tabCompletionIndex + 1) % tabCompletions.length;
completeCurrentWord(tabCompletions[tabCompletionIndex]);
}
}
function completeCurrentWord(completion) {
const words = currentCommand.split(' ');
words[words.length - 1] = completion;
const newCommand = words.join(' ');
// Clear current line and write new command
term.write('\x1b[2K\r');
showPrompt();
term.write(newCommand);
currentCommand = newCommand;
console.log('🔤 Tab completed to:', JSON.stringify(newCommand));
}
function getTabCompletions(prefix) {
// DISABLED: Cache-based completions disabled
// Use only basic command completion for now
const cachedCompletions = getCachedCompletions(prefix);
if (cachedCompletions.length > 0) {
return Promise.resolve(cachedCompletions);
}
// Fallback to server request
return fetch('/php-backend/api/terminal-complete.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prefix: prefix,
command: currentCommand,
session_id: sessionId
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.data && data.data.completions) {
return data.data.completions;
}
return [];
})
.catch(error => {
console.error('Tab completion failed:', error);
return getCachedCompletions(prefix); // Fallback to basic commands only
});
}
function getCachedCompletions(prefix) {
const words = currentCommand.split(' ');
const isFirstWord = words.length <= 1;
if (isFirstWord) {
// Command completion
const commands = ['ls', 'cd', 'pwd', 'cat', 'echo', 'grep', 'find', 'ps', 'df', 'du', 'uname', 'date', 'which', 'whoami', 'clear', 'history', 'mkdir', 'rmdir', 'rm', 'cp', 'mv', 'chmod', 'chown', 'head', 'tail', 'less', 'more', 'wc', 'sort', 'uniq'];
return commands.filter(cmd => cmd.startsWith(prefix.toLowerCase()));
} else {
// DISABLED: File completion from cache disabled
// Live file completion would require fresh ls call
}
return [];
}
function parseLsOutput(lsOutput) {
if (!lsOutput) return [];
const lines = lsOutput.split('\n');
const files = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('total')) continue;
// Parse ls -la format: permissions user group size date filename
const parts = trimmed.split(/\s+/);
if (parts.length >= 9) {
const filename = parts.slice(8).join(' ');
if (filename !== '.' && filename !== '..') {
files.push(filename);
}
}
}
return files;
}
function resetTabCompletion() {
tabCompletions = [];
tabCompletionIndex = -1;
lastTabPrefix = '';
}
function fitAndResize() {
appContainer.style.height = `${window.visualViewport.height}px`;
fitAddon.fit();
}
// Initialize terminal
initializeTerminal();
// Terminal input handling
term.onData((data) => {
try {
// If command is executing, buffer the input instead of processing it
if (isExecuting) {
pendingInput += data;
console.log('⏳ Buffering input during execution:', JSON.stringify(data));
return;
}
// Handle special key sequences
if (handleSpecialKeys(data)) {
return;
}
// Handle regular characters
if (data === '\x7f') { // Backspace
if (currentCommand.length > 0) {
currentCommand = currentCommand.slice(0, -1);
term.write('\b \b');
resetTabCompletion();
}
} else if (data === '\r') { // Enter
console.log('📤 Executing command:', JSON.stringify(currentCommand));
executeCommand(currentCommand);
clearCurrentCommand();
} else if (data === '\t') { // Tab key
handleTabCompletion();
} else if (data.charCodeAt(0) >= 32) { // Printable characters
currentCommand += data;
term.write(data);
console.log('⌨️ Current command buffer:', JSON.stringify(currentCommand));
// Reset tab completion when user types
resetTabCompletion();
}
} catch (error) {
console.error('Terminal input error:', error);
term.write('\r\n\x1b[31mInput error occurred\x1b[0m\r\n');
clearCurrentCommand();
showPrompt();
}
});
function processPendingInput() {
if (pendingInput) {
console.log('🔄 Processing pending input:', JSON.stringify(pendingInput));
// Process buffered input character by character
for (let i = 0; i < pendingInput.length; i++) {
const char = pendingInput[i];
if (char.charCodeAt(0) >= 32) { // Only printable characters
currentCommand += char;
term.write(char);
}
}
pendingInput = '';
}
}
function clearCurrentCommand() {
currentCommand = '';
console.log('🧹 Command buffer cleared');
}
// Virtual keyboard handling
document.getElementById('keyboard-bar').addEventListener('click', (e) => {
if (e.target.classList.contains('key-btn')) {
e.preventDefault();
const keyName = e.target.dataset.keyName;
const sequence = keyMap[keyName];
if (sequence) {
handleSpecialKeys(sequence);
}
term.focus();
}
});
// Focus terminal on body click
document.body.addEventListener('click', (e) => {
if (e.target.classList.contains('key-btn')) return;
term.focus();
}, true);
// Handle window resize
window.visualViewport.addEventListener('resize', fitAndResize);
term.focus();
// Load version info for footer
loadVersionInfo();
}
// Load version info
async function loadVersionInfo() {
try {
const response = await fetch('/build-info.json');
const buildInfo = await response.json();
if (buildInfo) {
const buildDate = new Date(buildInfo.buildTime).toLocaleDateString();
// Add version info to page title or somewhere visible
document.title = `Terminal - Shell Mirror v${buildInfo.version}`;
}
} catch (error) {
console.log('Could not load build info:', error);
}
}
</script>
</body>
</html>