UNPKG

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.

590 lines (512 loc) 21.7 kB
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Terminal Mirror</title> <!-- Google Analytics 4 --> <script> // Initialize dataLayer and gtag function first window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-LG7ZGLB8FK'); // Load gtag script with proper error handling (function() { var script = document.createElement('script'); script.async = true; script.src = 'https://www.googletagmanager.com/gtag/js?id=G-LG7ZGLB8FK'; script.onload = function() { console.log('✅ Google Analytics script loaded successfully'); window.gtagLoaded = true; }; script.onerror = function() { console.warn('❌ Failed to load Google Analytics script'); window.gtagLoaded = false; }; document.head.appendChild(script); })(); // Google Analytics helper function function sendGAEvent(eventName, eventParams) { if (typeof gtag === 'function') { console.log('📊 [GA] Sending event:', eventName, eventParams); gtag('event', eventName, eventParams); return true; } else { console.warn('❌ [GA] gtag not available, event not sent:', eventName); return false; } } </script> <!-- Microsoft Clarity --> <script type="text/javascript"> (function(c,l,a,r,i,t,y){ c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); })(window, document, "clarity", "script", "sy1w2d7il7"); </script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.15.0/css/xterm.css" /> <script src="https://cdn.jsdelivr.net/npm/xterm@4.15.0/lib/xterm.js"></script> <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script> <style> body, html { margin: 0; padding: 0; height: 100%; overflow: hidden; background-color: #1e1e1e; color: #ccc; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } #terminal-container { display: none; height: 100%; width: 100%; background-color: #000000; } #terminal-container.show { display: flex; flex-direction: column; } /* Session Header - Unified Design */ .session-header { background: #2a2a2a; color: #ccc; padding: 8px 16px; border-bottom: 1px solid #444; display: flex; align-items: center; justify-content: space-between; font-size: 0.9em; z-index: 100; height: 40px; } .header-left { display: flex; align-items: center; gap: 10px; position: relative; } .header-center { flex: 1; display: flex; justify-content: center; } .header-right { display: flex; align-items: center; gap: 8px; } /* Connection Status Indicator */ .connection-status { width: 12px; height: 12px; border-radius: 50%; background: #ff4444; margin-right: 12px; flex-shrink: 0; } .connection-status.connecting { background: #ffaa44; animation: pulse 1.5s ease-in-out infinite; } .connection-status.connected { background: #44ff44; box-shadow: 0 0 6px rgba(68, 255, 68, 0.5); } /* Session Tab Bar */ .session-tab-bar { display: flex; gap: 4px; padding: 8px; background: #2a2a2a; overflow-x: auto; -webkit-overflow-scrolling: touch; flex: 1; align-items: center; } .session-tab-bar::-webkit-scrollbar { height: 4px; } .session-tab-bar::-webkit-scrollbar-thumb { background: #555; border-radius: 2px; } .session-tab { display: flex; align-items: center; gap: 4px; padding: 6px 8px; background: transparent; border: none; border-bottom: 3px solid transparent; color: #888; min-width: 100px; white-space: nowrap; transition: all 0.2s ease; flex-shrink: 0; border-radius: 6px 6px 0 0; } .session-tab:hover { background: #3a3a3a; color: #ccc; } .session-tab.active { /* Colors set by inline styles from JavaScript */ font-weight: 600; } .session-tab-name { flex: 1; padding: 2px 4px; } .session-tab-close { background: none; border: none; color: #666; font-size: 1.1rem; cursor: pointer; padding: 0 4px; line-height: 1; border-radius: 3px; opacity: 0; transition: all 0.2s ease; } .session-tab:hover .session-tab-close { opacity: 0.7; } .session-tab-close:hover { opacity: 1 !important; background: rgba(255, 100, 100, 0.3); color: #ff6b6b; } .session-tab-name { overflow: hidden; text-overflow: ellipsis; } .session-tab-new { padding: 6px 12px; background: #667eea; border-radius: 4px; color: white; border: none; cursor: pointer; margin-left: 4px; font-weight: 500; transition: all 0.2s ease; flex-shrink: 0; } .session-tab-new:hover { background: #5568d3; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } /* Header Buttons */ .header-btn { background: transparent; border: 1px solid #444; color: #ccc; padding: 6px 12px; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 6px; font-size: 0.9em; text-decoration: none; transition: all 0.2s ease; } .header-btn:hover { background: #3a3a3a; border-color: #667eea; } .help-btn .btn-text { display: none; } @media (min-width: 768px) { .help-btn .btn-text { display: inline; } } #terminal { padding: 8px; /* Mac Terminal.app padding */ background-color: #000000; height: calc(100% - 16px - 40px); /* Subtract session header height */ width: calc(100% - 16px); flex: 1; } #connect-container { padding: 2em; text-align: center; } #agent-id-input { font-size: 1.2em; padding: 8px; width: 400px; margin-bottom: 1em; } #connect-btn { font-size: 1.2em; padding: 10px 20px; } /* Help Modal - Tab Navigation (Dark Kraken Style) */ .help-tabs { display: flex; padding: 0; border-bottom: 1px solid #2a2b30; background: #0a0b0d; } .help-tab { flex: 1; padding: 12px 20px; background: transparent; border: none; border-bottom: 2px solid transparent; cursor: pointer; font-size: 0.85rem; color: #5a5f6a; transition: all 0.15s ease; } .help-tab:hover { color: #8a8f98; background: #141519; } .help-tab.active { color: #fff; border-bottom-color: #5d5fef; font-weight: 500; } /* Help Modal - Tab Content */ .help-tab-content { display: none; } .help-tab-content.active { display: block; } /* Help Modal - Content Sections */ .help-section { margin-bottom: 24px; } .help-section h4 { color: #333; font-size: 1rem; margin-bottom: 12px; } .help-section p { color: #333; line-height: 1.6; margin-bottom: 8px; } .help-section ul { list-style: none; padding: 0; margin: 8px 0; } .help-section li { padding: 4px 0; color: #333; line-height: 1.6; } /* Help Modal - Mobile Responsiveness */ @media (max-width: 768px) { .help-tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; } .help-tab { font-size: 0.8rem; padding: 10px 12px; white-space: nowrap; } } </style> </head> <body> <div id="connect-container"> <h2>Terminal Mirror</h2> <p>Connecting to terminal...</p> </div> <div id="terminal-container"> <div class="session-header" id="session-header"> <!-- Connection Status Indicator --> <div class="connection-status" id="connection-status"></div> <!-- Session Tab Bar --> <div class="session-tab-bar" id="session-tab-bar"> <!-- Tabs will be rendered here by JavaScript --> </div> <!-- Header Right Section --> <div class="header-right"> <button class="header-btn help-btn" onclick="showHelpModal()"> <span>📖</span> <span class="btn-text">Help</span> </button> <a href="/app/dashboard.html" class="header-btn dashboard-btn"> <span class="btn-text">Dashboard</span> </a> </div> </div> <div id="terminal"></div> </div> <!-- Help Modal (Dark Kraken Style) --> <div id="help-modal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.85); align-items: center; justify-content: center; z-index: 20000;"> <div style="background: #141519; border-radius: 8px; max-width: 500px; width: 90%; max-height: 80vh; overflow: hidden; border: 1px solid #2a2b30; box-shadow: 0 8px 32px rgba(0,0,0,0.5);"> <!-- Header --> <div style="padding: 16px 20px; border-bottom: 1px solid #2a2b30; display: flex; justify-content: space-between; align-items: center;"> <h3 style="margin: 0; font-size: 1rem; color: #fff; font-weight: 600;">Shell Mirror Terminal</h3> <button onclick="closeHelpModal()" style="background: none; border: none; font-size: 1.2rem; cursor: pointer; padding: 4px 8px; border-radius: 4px; color: #5a5f6a; transition: all 0.15s;">×</button> </div> <!-- Tab Navigation --> <div class="help-tabs" style="display: flex; border-bottom: 1px solid #2a2b30; background: #0a0b0d;"> <button class="help-tab active" onclick="showHelpTab('sessions')" style="flex: 1; padding: 12px; background: none; border: none; color: #8a8f98; cursor: pointer; font-size: 0.85rem; border-bottom: 2px solid transparent;">Sessions</button> <button class="help-tab" onclick="showHelpTab('help')" style="flex: 1; padding: 12px; background: none; border: none; color: #8a8f98; cursor: pointer; font-size: 0.85rem; border-bottom: 2px solid transparent;">Troubleshooting</button> </div> <!-- Tab Content --> <div style="padding: 20px; max-height: 60vh; overflow-y: auto; color: #8a8f98; font-size: 0.85rem;"> <!-- Sessions Tab --> <div id="tab-sessions" class="help-tab-content active"> <p style="margin-top: 0; margin-bottom: 16px; color: #8a8f98;">Sessions let you run multiple terminals simultaneously.</p> <h4 style="margin-top: 16px; margin-bottom: 8px; color: #fff; font-size: 0.85rem; font-weight: 500;">Creating</h4> <ul style="list-style: none; padding: 0; margin: 0; color: #5a5f6a;"> <li style="padding: 3px 0;">• Tap "Sessions" → "+ New Session"</li> <li style="padding: 3px 0;">• Each session runs independently</li> </ul> <h4 style="margin-top: 16px; margin-bottom: 8px; color: #fff; font-size: 0.85rem; font-weight: 500;">Switching</h4> <ul style="list-style: none; padding: 0; margin: 0; color: #5a5f6a;"> <li style="padding: 3px 0;">• Tap "Sessions" dropdown</li> <li style="padding: 3px 0;">• Select any session</li> <li style="padding: 3px 0;">• Your processes keep running in background</li> </ul> <h4 style="margin-top: 16px; margin-bottom: 8px; color: #fff; font-size: 0.85rem; font-weight: 500;">Limits</h4> <ul style="list-style: none; padding: 0; margin: 0; color: #5a5f6a;"> <li style="padding: 3px 0;">• Max 10 sessions per Mac</li> <li style="padding: 3px 0;">• Auto-deleted after 24h inactive</li> </ul> </div> <!-- Troubleshooting Tab --> <div id="tab-help" class="help-tab-content" style="display: none;"> <h4 style="margin-top: 0; margin-bottom: 8px; color: #fff; font-size: 0.85rem; font-weight: 500;">Connection stuck?</h4> <ul style="list-style: none; padding: 0; margin: 0 0 16px 0; color: #5a5f6a;"> <li style="padding: 3px 0;">• Wait 10 seconds for auto-retry</li> <li style="padding: 3px 0;">• Still red? Return to Dashboard → Reconnect</li> </ul> <h4 style="margin-top: 16px; margin-bottom: 8px; color: #fff; font-size: 0.85rem; font-weight: 500;">Slow connection?</h4> <ul style="list-style: none; padding: 0; margin: 0 0 16px 0; color: #5a5f6a;"> <li style="padding: 3px 0;">• App tries: Local network → WebRTC → Fallback</li> <li style="padding: 3px 0;">• First connect may take 30 seconds</li> <li style="padding: 3px 0;">• WebRTC works best (100-500ms)</li> </ul> <h4 style="margin-top: 16px; margin-bottom: 8px; color: #fff; font-size: 0.85rem; font-weight: 500;">Behind corporate firewall?</h4> <ul style="list-style: none; padding: 0; margin: 0; color: #5a5f6a;"> <li style="padding: 3px 0;">• May need IT to whitelist STUN servers</li> <li style="padding: 3px 0;">• Contact: stun.l.google.com port 19302</li> </ul> </div> </div> </div> </div> <script> // Help Modal Functions function showHelpModal() { const modal = document.getElementById('help-modal'); modal.style.display = 'flex'; } function closeHelpModal() { const modal = document.getElementById('help-modal'); modal.style.display = 'none'; } function showHelpTab(tabName) { // Hide all tab content document.querySelectorAll('.help-tab-content').forEach(content => { content.classList.remove('active'); content.style.display = 'none'; }); // Remove active from all tab buttons document.querySelectorAll('.help-tab').forEach(btn => { btn.classList.remove('active'); }); // Show selected tab content const selectedContent = document.getElementById('tab-' + tabName); if (selectedContent) { selectedContent.classList.add('active'); selectedContent.style.display = 'block'; } // Activate corresponding button event.target.classList.add('active'); } // Close modal when clicking outside document.addEventListener('click', function(event) { const modal = document.getElementById('help-modal'); if (event.target === modal) { closeHelpModal(); } const closeModal = document.getElementById('close-session-modal'); if (event.target === closeModal) { hideCloseSessionModal(); } }); </script> <!-- Close Session Confirmation Modal --> <div id="close-session-modal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.85); align-items: center; justify-content: center; z-index: 20000;"> <div style="background: #2a2a2a; border-radius: 12px; max-width: 400px; width: 90%; overflow: hidden; border: 1px solid #444; box-shadow: 0 10px 40px rgba(0,0,0,0.5);"> <!-- Header --> <div style="padding: 20px 24px; border-bottom: 1px solid #444; display: flex; justify-content: space-between; align-items: center;"> <h3 style="margin: 0; font-size: 1.1rem; color: #fff;">Close Session?</h3> <button onclick="hideCloseSessionModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #888;">&times;</button> </div> <!-- Content --> <div style="padding: 24px;"> <div id="close-session-info" style="display: flex; align-items: center; gap: 16px; margin-bottom: 20px; padding-left: 16px; border-left: 4px solid #888;"> <div style="font-size: 2.5rem;">🗑️</div> <div> <div id="close-session-name" style="font-size: 1.1rem; font-weight: 500; margin-bottom: 4px;">Session 1</div> <div id="close-session-duration" style="font-size: 0.85rem; color: #888;">Duration: 5 minutes</div> </div> </div> <p style="color: #bbb; margin: 0 0 24px 0; font-size: 0.9rem; line-height: 1.5;"> This will terminate the terminal session. Any running processes will be stopped. </p> <!-- Buttons --> <div style="display: flex; gap: 12px; justify-content: flex-end;"> <button onclick="hideCloseSessionModal()" style="padding: 10px 20px; background: #444; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: background 0.2s;">Cancel</button> <button id="confirm-close-session-btn" onclick="confirmCloseSession()" style="padding: 10px 20px; background: #dc3545; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem; font-weight: 500; transition: background 0.2s;">Close Session</button> </div> </div> </div> </div> <script> // Close Session Modal Functions let pendingCloseSessionId = null; function showCloseSessionModal(sessionId, sessionName, createdAt, sessionColor) { pendingCloseSessionId = sessionId; // Calculate duration const duration = createdAt ? formatDuration(Date.now() - createdAt) : 'Unknown'; document.getElementById('close-session-name').textContent = sessionName || 'Session'; document.getElementById('close-session-duration').textContent = `Duration: ${duration}`; // Apply session color to border and name const infoDiv = document.getElementById('close-session-info'); const nameDiv = document.getElementById('close-session-name'); if (sessionColor) { infoDiv.style.borderLeftColor = sessionColor.border; nameDiv.style.color = sessionColor.border; } else { infoDiv.style.borderLeftColor = '#888'; nameDiv.style.color = '#fff'; } document.getElementById('close-session-modal').style.display = 'flex'; } function hideCloseSessionModal() { document.getElementById('close-session-modal').style.display = 'none'; pendingCloseSessionId = null; } function confirmCloseSession() { if (pendingCloseSessionId) { // Call the actual close function from terminal.js doCloseSession(pendingCloseSessionId); } hideCloseSessionModal(); } function formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days}d ${hours % 24}h`; if (hours > 0) return `${hours}h ${minutes % 60}m`; if (minutes > 0) return `${minutes} minute${minutes !== 1 ? 's' : ''}`; return `${seconds} second${seconds !== 1 ? 's' : ''}`; } </script> <script src="/app/terminal.js?v=1.5.94"></script> </body> </html>