Files
SuperCharged-Claude-Code-Up…/public/claude-landing.html
uroma 55aafbae9a Fix project isolation: Make loadChatHistory respect active project sessions
- Modified loadChatHistory() to check for active project before fetching all sessions
- When active project exists, use project.sessions instead of fetching from API
- Added detailed console logging to debug session filtering
- This prevents ALL sessions from appearing in every project's sidebar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 14:43:05 +00:00

1028 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Code - AI-Powered Development</title>
<link rel="stylesheet" href="/css/style.css?v1769073300000">
<link rel="stylesheet" href="/claude-ide/sessions-landing.css?v1769073300000">
<style>
/* Hero Section - Enhanced */
.hero-section {
min-height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px 60px;
text-align: center;
background: linear-gradient(180deg, rgba(74, 158, 255, 0.05) 0%, transparent 100%);
}
.hero-title {
font-size: 64px;
font-weight: 700;
margin: 0 0 16px 0;
background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: -0.02em;
}
.hero-subtitle {
font-size: 20px;
color: #888;
margin: 0 0 48px 0;
font-weight: 400;
}
.hero-input-wrapper {
width: 100%;
max-width: 700px;
position: relative;
}
.hero-input {
width: 100%;
padding: 20px 28px;
font-size: 18px;
background: #1a1a1a;
border: 2px solid #333;
border-radius: 16px;
color: #e0e0e0;
outline: none;
transition: all 0.3s ease;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.hero-input:focus {
border-color: #4a9eff;
box-shadow: 0 0 0 4px rgba(74, 158, 255, 0.15), 0 8px 30px rgba(74, 158, 255, 0.2);
transform: translateY(-2px);
}
.hero-input::placeholder {
color: #666;
}
.hero-actions {
display: flex;
gap: 16px;
margin-top: 32px;
justify-content: center;
}
.btn-hero-primary {
padding: 14px 32px;
font-size: 16px;
font-weight: 600;
background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%);
border: none;
border-radius: 12px;
color: white;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(74, 158, 255, 0.3);
}
.btn-hero-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(74, 158, 255, 0.5);
}
.btn-hero-secondary {
padding: 14px 32px;
font-size: 16px;
font-weight: 600;
background: #2a2a2a;
border: 2px solid #444;
border-radius: 12px;
color: #e0e0e0;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-hero-secondary:hover {
background: #333;
border-color: #4a9eff;
}
/* Sessions Section - Enhanced */
.sessions-section {
padding: 60px 20px 100px;
max-width: 1400px;
margin: 0 auto;
}
.sessions-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.sessions-title {
font-size: 32px;
font-weight: 700;
color: #e0e0e0;
margin: 0;
}
.sessions-count {
color: #888;
font-size: 16px;
margin-left: 12px;
}
.btn-refresh {
padding: 10px 20px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 10px;
color: #e0e0e0;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-refresh:hover {
background: #252525;
border-color: #4a9eff;
}
/* Session Cards Grid */
.sessions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
}
.session-card {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 16px;
padding: 24px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.session-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #4a9eff, #a78bfa);
opacity: 0;
transition: opacity 0.3s ease;
}
.session-card:hover {
background: #252525;
border-color: #4a9eff;
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(74, 158, 255, 0.15);
}
.session-card:hover::before {
opacity: 1;
}
.session-card-header {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 16px;
}
.session-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.session-info {
flex: 1;
min-width: 0;
}
.session-name {
font-size: 18px;
font-weight: 600;
color: #e0e0e0;
margin: 0 0 4px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.session-id {
font-size: 12px;
color: #666;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
}
.session-meta {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.session-meta-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #888;
}
.session-meta-item .icon {
font-size: 16px;
}
.session-preview {
background: #0d0d0d;
border-radius: 10px;
padding: 16px;
font-size: 14px;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.session-status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
}
.session-status.active {
color: #51cf66;
background: rgba(81, 207, 102, 0.1);
}
.session-status.stopped {
color: #888;
background: rgba(136, 136, 136, 0.1);
}
.session-status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
/* Loading & Empty States */
.loading-state, .empty-state, .error-state {
text-align: center;
padding: 80px 20px;
}
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid #333;
border-top-color: #4a9eff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 24px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ============================================================
Session Options Modal
============================================================ */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 20px;
}
.modal-content {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
width: 100%;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
animation: modalSlideIn 0.2s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #333;
}
.modal-header h2 {
font-size: 20px;
font-weight: 600;
color: #e0e0e0;
margin: 0;
}
.modal-close {
background: none;
border: none;
color: #888;
font-size: 28px;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.2s ease;
}
.modal-close:hover {
background: #252525;
color: #e0e0e0;
}
.modal-body {
padding: 24px;
}
.session-options-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
.session-option-card {
background: #252525;
border: 2px solid #333;
border-radius: 12px;
padding: 24px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 12px;
}
.session-option-card:hover {
background: #2d2d2d;
border-color: #4a9eff;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(74, 158, 255, 0.15);
}
.option-icon {
font-size: 48px;
}
.option-title {
font-size: 18px;
font-weight: 600;
color: #e0e0e0;
}
.option-description {
font-size: 14px;
color: #888;
}
/* Responsive modal */
@media (max-width: 600px) {
.modal-content {
max-width: 100%;
margin: 20px;
}
.session-options-grid {
grid-template-columns: 1fr;
}
.modal-header,
.modal-body {
padding: 16px;
}
}
.empty-icon, .error-icon {
font-size: 64px;
margin-bottom: 20px;
}
.empty-title, .error-title {
font-size: 24px;
font-weight: 600;
color: #e0e0e0;
margin: 0 0 12px 0;
}
.empty-subtitle, .error-message {
font-size: 16px;
color: #888;
margin: 0 0 32px 0;
}
/* Responsive */
@media (max-width: 768px) {
.hero-title {
font-size: 40px;
}
.sessions-grid {
grid-template-columns: 1fr;
}
.hero-actions {
flex-direction: column;
width: 100%;
}
.btn-hero-primary, .btn-hero-secondary {
width: 100%;
}
}
</style>
</head>
<body class="sessions-page">
<!-- Hero Section -->
<section class="hero-section">
<h1 class="hero-title">Claude Code</h1>
<p class="hero-subtitle">Start coding with AI-powered development</p>
<div class="hero-input-wrapper">
<input
type="text"
id="session-input"
class="hero-input"
placeholder="What would you like to build today?"
autocomplete="off"
/>
</div>
<div class="hero-actions">
<button class="btn-hero-primary" onclick="showSessionOptionsModal()">
<span></span> Start New Session
</button>
<button class="btn-hero-secondary" onclick="selectFolder()">
<span>📁</span> Open Folder
</button>
</div>
</section>
<!-- Sessions Section -->
<section class="sessions-section">
<div class="sessions-header">
<div style="display: flex; align-items: baseline;">
<h2 class="sessions-title">Recent Sessions</h2>
<span id="sessions-count" class="sessions-count"></span>
</div>
<button class="btn-refresh" onclick="loadSessions()">
<span>🔄</span> Refresh
</button>
</div>
<!-- Loading State -->
<div id="sessions-loading" class="loading-state">
<div class="loading-spinner"></div>
<p style="color: #888;">Loading sessions...</p>
</div>
<!-- Error State -->
<div id="sessions-error" class="error-state" style="display: none;">
<div class="error-icon">⚠️</div>
<h3 class="error-title">Failed to load sessions</h3>
<p id="error-message" class="error-message">Please try again</p>
<button class="btn-hero-secondary" onclick="loadSessions()" style="margin-top: 16px;">Retry</button>
</div>
<!-- Empty State -->
<div id="sessions-empty" class="empty-state" style="display: none;">
<div class="empty-icon">💬</div>
<h3 class="empty-title">No sessions yet</h3>
<p class="empty-subtitle">Start a new session to begin coding with AI</p>
<button class="btn-hero-primary" onclick="startNewSession()">
<span></span> Create Your First Session
</button>
</div>
<!-- Sessions Grid -->
<div id="sessions-grid" class="sessions-grid" style="display: none;">
<!-- Session cards rendered dynamically -->
</div>
</section>
<script>
// ============================================================
// Sessions Landing Page - Enhanced
// ============================================================
let sessions = [];
// Load on page load
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
initializeHero();
loadSessions();
initializeKeyboardShortcuts();
});
// Initialize hero input
function initializeHero() {
const input = document.getElementById('session-input');
if (input) {
input.focus();
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const prompt = input.value.trim();
if (prompt) {
startNewSessionWithPrompt(prompt);
}
}
});
}
}
// Initialize keyboard shortcuts
function initializeKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Cmd/Ctrl + N to start new session
if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
e.preventDefault();
document.getElementById('session-input')?.focus();
}
});
}
// Check authentication
async function checkAuth() {
try {
const res = await fetch('/claude/api/auth/status');
if (!res.ok) return;
const data = await res.json();
if (!data.authenticated) {
window.location.href = '/claude/login';
}
} catch (error) {
console.error('[checkAuth] Error:', error);
}
}
// Load sessions from server
async function loadSessions() {
const loading = document.getElementById('sessions-loading');
const empty = document.getElementById('sessions-empty');
const error = document.getElementById('sessions-error');
const grid = document.getElementById('sessions-grid');
const countEl = document.getElementById('sessions-count');
// Reset states
if (loading) loading.style.display = 'block';
if (empty) empty.style.display = 'none';
if (error) error.style.display = 'none';
if (grid) grid.style.display = 'none';
try {
console.log('[Landing] Loading sessions...');
const res = await fetch('/claude/api/claude/sessions');
if (!res.ok) {
throw new Error(`Failed to load sessions (HTTP ${res.status})`);
}
const data = await res.json();
console.log('[Landing] Sessions data:', data);
// Combine active and historical sessions
sessions = [
...(data.active || []).map(s => ({...s, type: 'active'})),
...(data.historical || []).map(s => ({...s, type: 'historical'}))
];
// Sort by last activity (newest first)
sessions.sort((a, b) => {
const dateA = new Date(a.lastActivity || a.createdAt || a.created_at);
const dateB = new Date(b.lastActivity || b.createdAt || b.created_at);
return dateB - dateA;
});
if (loading) loading.style.display = 'none';
if (sessions.length === 0) {
if (empty) empty.style.display = 'block';
} else {
if (grid) {
grid.style.display = 'grid';
renderSessionsGrid();
}
if (countEl) countEl.textContent = `(${sessions.length})`;
}
} catch (err) {
console.error('[Landing] Error loading sessions:', err);
if (loading) loading.style.display = 'none';
if (error) {
error.style.display = 'block';
document.getElementById('error-message').textContent = err.message;
}
}
}
// Render sessions grid
function renderSessionsGrid() {
const grid = document.getElementById('sessions-grid');
if (!grid) return;
grid.innerHTML = sessions.slice(0, 12).map(session => createSessionCard(session)).join('');
// Add click handlers
grid.querySelectorAll('.session-card').forEach(card => {
const sessionId = card.dataset.sessionId;
card.addEventListener('click', () => openSession(sessionId));
});
}
// Create a session card HTML
function createSessionCard(session) {
const name = getSessionName(session);
const preview = getSessionPreview(session);
const relativeTime = getRelativeTime(session);
const messageCount = session.messageCount || 0;
const isActive = session.type === 'active' && session.status === 'running';
return `
<div class="session-card" data-session-id="${session.id}">
<div class="session-card-header">
<div class="session-icon">💬</div>
<div class="session-info">
<h3 class="session-name">${escapeHtml(name)}</h3>
<p class="session-id">${session.id.substring(0, 20)}...</p>
</div>
<span class="session-status ${isActive ? 'active' : 'stopped'}">
<span class="session-status-dot"></span>
${isActive ? 'Active' : 'Stopped'}
</span>
</div>
<div class="session-meta">
<div class="session-meta-item">
<span class="icon">🕐</span>
<span>${relativeTime}</span>
</div>
<div class="session-meta-item">
<span class="icon">💬</span>
<span>${messageCount} messages</span>
</div>
</div>
<div class="session-preview">
${escapeHtml(preview)}
</div>
</div>
`;
}
// Get session name
function getSessionName(session) {
return session.metadata?.project ||
session.workingDir?.split('/').pop() ||
`Session ${session.id.substring(0, 8)}`;
}
// Get session preview
function getSessionPreview(session) {
if (session.lastMessage) {
return session.lastMessage.length > 150
? session.lastMessage.substring(0, 150) + '...'
: session.lastMessage;
}
return 'No messages yet';
}
// Get relative time
function getRelativeTime(session) {
const date = new Date(session.lastActivity || session.createdAt || session.created_at);
const now = new Date();
const diffMins = Math.floor((now - date) / 60000);
const diffHours = Math.floor((now - date) / 3600000);
const diffDays = Math.floor((now - date) / 86400000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString();
}
// Open a session
function openSession(sessionId) {
window.location.href = `/claude/ide/session/${sessionId}`;
}
// ============================================================
// Session Creation - Shared Function
// ============================================================
/**
* Create a session with a specific working directory
* This is the shared function used by both Start New Session and Open Folder
* @param {string} workingDir - The working directory for the session
* @param {object} metadata - Optional metadata to attach to the session
* @param {string} initialPrompt - Optional initial prompt to send
*/
async function createSessionWithDir(workingDir, metadata = {}, initialPrompt = null) {
try {
showLoadingOverlay('Creating session...');
const requestBody = {
workingDir: workingDir,
metadata: {
type: 'chat',
source: metadata.source || 'landing-page',
...metadata
}
};
const res = await fetch('/claude/api/claude/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
if (!res.ok) {
throw new Error(`Failed to create session (HTTP ${res.status})`);
}
const data = await res.json();
// Backend returns: { id, status, createdAt, workingDir, metadata }
if (data && data.id) {
await new Promise(resolve => setTimeout(resolve, 300));
// Build URL with prompt if provided
let url = `/claude/ide/session/${data.id}`;
if (initialPrompt) {
url += `?prompt=${encodeURIComponent(initialPrompt)}`;
}
window.location.href = url;
} else {
throw new Error('Invalid response from server');
}
} catch (error) {
console.error('Error creating session:', error);
hideLoadingOverlay();
showToast(error.message || 'Failed to create session', 'error');
}
}
// ============================================================
// Start New Session Button - Shows Options Modal
// ============================================================
/**
* Show session creation options modal
* User can choose between Quick Start (home dir) or Choose Folder
*/
function showSessionOptionsModal() {
// Remove existing modal if present
const existingModal = document.getElementById('session-options-modal');
if (existingModal) existingModal.remove();
// Create modal
const modal = document.createElement('div');
modal.id = 'session-options-modal';
modal.innerHTML = `
<div class="modal-overlay" onclick="closeSessionOptionsModal()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2>Choose Session Type</h2>
<button class="modal-close" onclick="closeSessionOptionsModal()">&times;</button>
</div>
<div class="modal-body">
<p style="color: #888; margin-bottom: 24px;">How would you like to start your session?</p>
<div class="session-options-grid">
<button class="session-option-card" onclick="quickStartSession()">
<div class="option-icon">🏠</div>
<div class="option-title">Quick Start</div>
<div class="option-description">Use your home directory</div>
</button>
<button class="session-option-card" onclick="chooseFolderSession()">
<div class="option-icon">📁</div>
<div class="option-title">Choose Folder</div>
<div class="option-description">Select a custom directory</div>
</button>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
document.body.style.overflow = 'hidden';
}
/**
* Close the session options modal
*/
function closeSessionOptionsModal() {
const modal = document.getElementById('session-options-modal');
if (modal) {
modal.remove();
document.body.style.overflow = '';
}
}
/**
* Quick Start - Create session with home directory
*/
function quickStartSession() {
closeSessionOptionsModal();
// Use home directory or vault path as default
createSessionWithDir(
'/home/uroma', // Default working directory
{ source: 'landing-page-quick-start' }
);
}
/**
* Choose Folder - Open folder explorer modal
*/
function chooseFolderSession() {
closeSessionOptionsModal();
selectFolder();
}
// ============================================================
// Start New Session with Prompt (from hero input)
// ============================================================
/**
* Start new session with an initial prompt
* Shows options modal, then creates session with prompt
* @param {string} prompt - The initial prompt to send
*/
async function startNewSessionWithPrompt(prompt) {
// For prompt-based creation, we'll use home directory directly
// (user can change working dir later if needed)
await createSessionWithDir(
'/home/uroma',
{ source: 'landing-page-prompt' },
prompt
);
}
// ============================================================
// Select Folder (Open Folder Button)
// ============================================================
/**
* Select folder and create session
* This loads the folder explorer modal
* Note: folder-explorer-modal.js handles the actual session creation
* after folder selection via its selectFolder() function
*/
async function selectFolder() {
try {
if (!window.FolderExplorer) {
// Fix: Use correct path with /claude prefix since landing page is at /claude/
await loadScript('/claude/claude-ide/components/folder-explorer-modal.js?v1769073300000');
}
if (window.FolderExplorer) {
window.FolderExplorer.show();
} else {
showToast('Failed to load folder explorer', 'error');
}
} catch (error) {
console.error('Error opening folder explorer:', error);
showToast('Failed to load folder explorer', 'error');
}
}
// Utility functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text || '';
return div.innerHTML;
}
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function showLoadingOverlay(message) {
// Simple loading indicator
const overlay = document.createElement('div');
overlay.id = 'loading-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
`;
overlay.innerHTML = `
<div style="text-align: center; color: white;">
<div class="loading-spinner"></div>
<p style="margin-top: 20px;">${message}</p>
</div>
`;
document.body.appendChild(overlay);
}
function hideLoadingOverlay() {
const overlay = document.getElementById('loading-overlay');
if (overlay) overlay.remove();
}
function showToast(message, type = 'info') {
// Simple toast notification
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
bottom: 24px;
right: 24px;
padding: 16px 24px;
background: ${type === 'error' ? '#ff6b6b' : '#4a9eff'};
color: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 9999;
animation: slideIn 0.3s ease;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
</script>
</body>
</html>