UNPKG

claude-code-web

Version:

Web-based interface for Claude Code CLI accessible via browser

402 lines (377 loc) 21.4 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Claude Code Web Interface</title> <!-- PWA Meta Tags --> <meta name="description" content="Web interface for Claude Code - Run Claude commands directly from your browser"> <meta name="theme-color" content="#161b22"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-title" content="CC Web"> <meta name="mobile-web-app-capable" content="yes"> <meta name="application-name" content="Claude Code Web"> <meta name="format-detection" content="telephone=no"> <!-- Web App Manifest --> <link rel="manifest" href="/manifest.json"> <!-- Icons for various platforms --> <link rel="icon" type="image/png" sizes="32x32" href="/icon-32.png"> <link rel="icon" type="image/png" sizes="16x16" href="/icon-16.png"> <link rel="apple-touch-icon" sizes="180x180" href="/icon-180.png"> <meta name="msapplication-TileColor" content="#161b22"> <meta name="msapplication-TileImage" content="/icon-144.png"> <!-- Apply saved theme early to avoid flash --> <script> (function() { try { const s = JSON.parse(localStorage.getItem('cc-web-settings') || '{}'); if (s && s.theme === 'light') { document.documentElement.setAttribute('data-theme', 'light'); } else { // Dark is default — ensure no 'light' override is set document.documentElement.removeAttribute('data-theme'); } } catch (_) { /* ignore */ } })(); </script> <link rel="stylesheet" href="style.css"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> <script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script> <script src="https://unpkg.com/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script> <script src="https://unpkg.com/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js"></script> <link rel="stylesheet" href="https://unpkg.com/xterm@5.3.0/css/xterm.css" /> </head> <body> <div id="app"> <!-- Session Tabs Bar --> <div class="session-tabs-bar" id="sessionTabsBar"> <div class="tabs-section"> <div class="tabs-container" id="tabsContainer"> <!-- Tabs will be dynamically added here --> </div> <button class="tab-new" id="tabNewBtn" title="New Session (Ctrl+T)"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <line x1="12" y1="5" x2="12" y2="19"/> <line x1="5" y1="12" x2="19" y2="12"/> </svg> </button> </div> <!-- Overflow dropdown for mobile - outside tabs-section to prevent scrolling issues --> <div class="tab-overflow-wrapper" id="tabOverflowWrapper" style="display: none;"> <button class="tab-overflow-btn" id="tabOverflowBtn" title="More tabs"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="5" r="1"/> <circle cx="12" cy="12" r="1"/> <circle cx="12" cy="19" r="1"/> </svg> <span class="tab-overflow-count">0</span> </button> <div class="tab-overflow-menu" id="tabOverflowMenu"> <!-- Overflow tabs will be listed here --> </div> </div> <div class="tab-actions"> <button class="tab-settings" id="settingsBtn" title="Settings"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1Z"/> </svg> </button> </div> </div> <div class="mobile-menu" id="mobileMenu"> <div class="mobile-menu-header"> <h2><span class="icon" aria-hidden="true"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="4"/><path d="M8 10h.01M16 10h.01M8 16c1.333-1 2.667-1 4 0 1.333 1 2.667 1 4 0"/></svg></span> Claude Code</h2> <button class="close-menu-btn" id="closeMenuBtn">&times;</button> </div> <div class="mobile-menu-content"> <button id="sessionsBtnMobile" class="mobile-menu-btn">Sessions</button> <button id="closeSessionBtnMobile" class="mobile-menu-btn btn-danger" style="display: none;">Close Session</button> <button id="reconnectBtnMobile" class="mobile-menu-btn" disabled>Reconnect</button> <button id="clearBtnMobile" class="mobile-menu-btn">Clear Terminal</button> <button id="settingsBtnMobile" class="mobile-menu-btn">Settings</button> </div> </div> <main class="main"> <div class="terminal-container" id="terminalContainer" data-view="single"> <div class="terminal-wrapper"> <div id="terminal"></div> </div> </div> </main> <!-- App-wide overlay (moved out of terminal container to avoid width issues) --> <div class="terminal-overlay" id="overlay" style="display: none;"> <div class="overlay-content"> <div class="loading-spinner" id="loadingSpinner"> <div class="spinner"></div> <p>Connecting to Claude Code...</p> </div> <div class="start-prompt" id="startPrompt" style="display: none;"> <h2>Choose Your Assistant</h2> <p>Select one to start in this directory.</p> <div class="start-buttons"> <button id="startBtn" class="btn btn-primary">Start Claude</button> <button id="dangerousSkipBtn" class="btn btn-danger" title="Dangerously skip permissions. Use with caution.">Dangerous Claude</button> <button id="startCodexBtn" class="btn btn-primary">Start Codex</button> <button id="dangerousCodexBtn" class="btn btn-danger" title="Bypass approvals and sandbox. Use with caution.">Dangerous Codex</button> <button id="startAgentBtn" class="btn btn-primary">Start Cursor</button> </div> </div> <div class="error-message" id="errorMessage" style="display: none;"> <h3>Connection Error</h3> <p id="errorText"></p> <button id="retryBtn" class="btn btn-primary">Retry</button> </div> </div> </div> <div class="settings-modal" id="settingsModal"> <div class="modal-content"> <div class="modal-header"> <h2>Settings</h2> <button class="close-btn" id="closeSettingsBtn">&times;</button> </div> <div class="modal-body"> <div class="setting-group"> <label for="fontSize">Font Size:</label> <input type="range" id="fontSize" min="10" max="24" value="14"> <span id="fontSizeValue">14px</span> </div> <div class="setting-group"> <label for="themeSelect">Theme:</label> <select id="themeSelect"> <option value="dark">Dark</option> <option value="light">Light</option> </select> </div> <div class="setting-group"> <label for="showTokenStats">Show Token Stats:</label> <input type="checkbox" id="showTokenStats" checked> </div> </div> <div class="modal-footer"> <button class="btn btn-primary" id="saveSettingsBtn">Save Settings</button> </div> </div> </div> <div class="plan-modal" id="planModal"> <div class="modal-content"> <div class="modal-header"> <h2> <span class="icon" aria-hidden="true"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="4" y="3" width="16" height="18" rx="2"/> <line x1="8" y1="7" x2="16" y2="7"/> </svg> </span> Claude's Plan </h2> <button class="close-btn" id="closePlanBtn" title="Close">&times;</button> </div> <div class="modal-body"> <div class="plan-status" id="planStatus"> <span class="plan-status-indicator"></span> <span class="plan-status-text">Plan Mode Active</span> </div> <div class="plan-content" id="planContent"> <!-- Plan will be inserted here --> </div> </div> <div class="modal-footer"> <button class="btn btn-success" id="acceptPlanBtn"> <span class="icon" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="20 6 9 17 4 12"/> </svg> </span> Accept Plan </button> <button class="btn btn-danger" id="rejectPlanBtn"> <span class="icon" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <line x1="18" y1="6" x2="6" y2="18"/> <line x1="6" y1="6" x2="18" y2="18"/> </svg> </span> Reject Plan </button> </div> </div> </div> <div class="session-modal" id="newSessionModal"> <div class="modal-content"> <div class="modal-header"> <h2>Create New Session</h2> <button class="close-btn" id="closeNewSessionBtn">&times;</button> </div> <div class="modal-body"> <div class="form-group"> <label for="sessionName">Session Name:</label> <input type="text" id="sessionName" placeholder="Enter session name (optional)"> </div> <div class="form-group"> <label for="sessionWorkingDir">Working Directory:</label> <input type="text" id="sessionWorkingDir" placeholder="Leave empty to use current directory"> </div> </div> <div class="modal-footer"> <button class="btn btn-secondary" id="cancelNewSessionBtn">Cancel</button> <button class="btn btn-primary" id="createSessionBtn">Create Session</button> </div> </div> </div> <div class="folder-browser-modal" id="folderBrowserModal"> <div class="modal-content folder-browser-content"> <div class="modal-header"> <h2>Select Working Directory</h2> </div> <div class="folder-browser-body"> <div class="folder-path-bar"> <button class="btn-icon" id="folderUpBtn" title="Go to parent directory"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M19 12H5M12 19l-7-7 7-7"/> </svg> </button> <button class="btn-icon" id="folderHomeBtn" title="Go to home directory"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> </svg> </button> <input type="text" class="folder-path-input" id="currentPathInput" readonly> <button class="btn-icon" id="createFolderBtn" title="Create new folder"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/> <line x1="12" y1="11" x2="12" y2="17"/> <line x1="9" y1="14" x2="15" y2="14"/> </svg> </button> </div> <div class="folder-create-bar" id="folderCreateBar" style="display: none;"> <input type="text" class="folder-name-input" id="newFolderNameInput" placeholder="Enter folder name..."> <button class="btn btn-primary btn-small" id="confirmCreateFolderBtn">Create</button> <button class="btn btn-secondary btn-small" id="cancelCreateFolderBtn">Cancel</button> </div> <div class="folder-list-container"> <div class="folder-list" id="folderList"> <!-- Folders will be dynamically added here --> </div> </div> <div class="folder-browser-footer"> <label class="checkbox-label"> <input type="checkbox" id="showHiddenFolders"> Show hidden folders </label> <div class="folder-browser-buttons"> <button class="btn btn-secondary" id="cancelFolderBtn">Cancel</button> <button class="btn btn-primary" id="selectFolderBtn">Select This Folder</button> </div> </div> </div> </div> </div> </div> <!-- Mobile Sessions Modal --> <div class="session-modal" id="mobileSessionsModal"> <div class="modal-content"> <div class="modal-header"> <h2>Sessions</h2> <button class="modal-close" id="closeMobileSessionsModal">&times;</button> </div> <div class="modal-body"> <div id="mobileSessionList" class="session-list"> <!-- Sessions will be dynamically added here --> </div> <button class="btn btn-primary" id="newSessionBtnMobile" style="width: 100%; margin-top: 10px;">New Session</button> </div> </div> </div> <script src="auth.js"></script> <script src="plan-detector.js"></script> <script src="session-manager.js"></script> <script src="splits.js"></script> <script src="icons.js"></script> <script src="app.js"></script> <!-- Service Worker Registration --> <script> if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('ServiceWorker registration successful:', registration.scope); // Check for updates periodically setInterval(() => { registration.update(); }, 60000); // Check every minute // Handle updates registration.addEventListener('updatefound', () => { const newWorker = registration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // New service worker available, prompt user to refresh if (confirm('A new version of Claude Code Web is available. Refresh to update?')) { newWorker.postMessage({ type: 'SKIP_WAITING' }); window.location.reload(); } } }); }); }) .catch(err => { console.log('ServiceWorker registration failed:', err); }); }); } // Handle app install prompt let deferredPrompt; window.addEventListener('beforeinstallprompt', (e) => { // Prevent Chrome 67 and earlier from automatically showing the prompt e.preventDefault(); // Stash the event so it can be triggered later deferredPrompt = e; // Create install button if it doesn't exist if (!document.getElementById('installBtn')) { const installBtn = document.createElement('button'); installBtn.id = 'installBtn'; installBtn.className = 'install-btn'; installBtn.innerHTML = '<span class="icon" aria-hidden="true"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12"/><path d="M8 11l4 4 4-4"/><path d="M5 21h14"/></svg></span> Install App'; installBtn.style.cssText = ` position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; background: var(--accent); color: white; border: none; border-radius: 8px; cursor: pointer; font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, 'JetBrains Mono', monospace; font-size: 14px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); z-index: 1000; transition: all 0.3s ease; `; installBtn.addEventListener('click', async () => { if (deferredPrompt) { deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; console.log(`User response to install prompt: ${outcome}`); deferredPrompt = null; installBtn.remove(); } }); document.body.appendChild(installBtn); } }); // Handle successful installation window.addEventListener('appinstalled', () => { console.log('PWA was installed'); const installBtn = document.getElementById('installBtn'); if (installBtn) { installBtn.remove(); } }); </script> </body> </html>