claude-code-web
Version:
Web-based interface for Claude Code CLI accessible via browser
379 lines (355 loc) • 19.9 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">
<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&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">
<!-- Usage Stats Bar -->
<div class="usage-stats-bar" id="usageStatsContainer">
<div class="usage-stat" data-label="Tokens:">
<span class="stat-label">Tokens:</span>
<span class="stat-value" id="usageTokens">0</span>
</div>
<div class="usage-stat" data-label="Cost:">
<span class="stat-label">Cost:</span>
<span class="stat-value" id="usageCost">$0.00</span>
</div>
<div class="usage-stat" data-label="Rate:">
<span class="stat-label">Rate:</span>
<span class="stat-value" id="usageRate">0 tok/min</span>
</div>
<div class="usage-stat" data-label="Reset:">
<span class="stat-label">Reset:</span>
<span class="stat-value" id="usageTitle">Loading...</span>
</div>
<div class="usage-stat" data-label="Model:">
<span class="stat-label">Model:</span>
<span class="stat-value" id="usageModel">-</span>
</div>
<!-- Token usage progress bar -->
<div class="usage-progress" id="usageProgress" style="display: none;">
<div class="usage-progress-bar" id="usageProgressBar"></div>
<div class="usage-progress-text" id="usageProgressText">0%</div>
</div>
</div>
<!-- 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>
<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 class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<h2>🤖 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">
<div class="terminal-wrapper">
<div id="terminal"></div>
</div>
<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>
</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>
</main>
<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="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>📋 Claude's Plan</h2>
<button class="close-btn" id="closePlanBtn">×</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">✅ Accept Plan</button>
<button class="btn btn-danger" id="rejectPlanBtn">❌ 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="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 = '📥 Install App';
installBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
padding: 12px 20px;
background: var(--accent-color, #ff6b00);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-family: '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>