claude-code-web
Version:
Web-based interface for Claude Code CLI accessible via browser
402 lines (377 loc) • 21.4 kB
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">×</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">×</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">×</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">×</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">×</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>