Files
SuperCharged-Claude-Code-Up…/public/claude-ide/sessions-landing.js
uroma 0dd2083556 Initial commit: Obsidian Web Interface for Claude Code
- Full IDE with terminal integration using xterm.js
- Session management with local and web sessions
- HTML preview functionality
- Multi-terminal support with session picker

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 16:29:44 +00:00

609 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Sessions Landing Page JavaScript
*/
// Load sessions on page load
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
initializeProjectInput();
loadSessions();
});
// Check authentication
async function checkAuth() {
try {
const res = await fetch('/claude/api/auth/status');
if (!res.ok) {
throw new Error('Request failed');
}
const data = await res.json();
if (!data.authenticated) {
// Redirect to login if not authenticated
window.location.href = '/claude/login.html';
}
} catch (error) {
console.error('Auth check failed:', error);
}
}
/**
* Initialize project input field in hero section
*/
function initializeProjectInput() {
const input = document.getElementById('project-input');
const status = document.getElementById('input-status');
if (!input) return;
// Auto-focus on page load
input.focus();
input.addEventListener('input', () => {
const projectName = input.value.trim();
const hasInvalidChars = !validateProjectName(projectName);
if (hasInvalidChars && projectName.length > 0) {
status.textContent = 'Invalid characters';
status.classList.add('error');
} else {
status.textContent = '';
status.classList.remove('error');
}
});
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const projectName = input.value.trim();
if (projectName && validateProjectName(projectName)) {
createProject(projectName);
}
}
});
}
/**
* Create a new project and navigate to IDE
*/
async function createProject(projectName) {
try {
showLoadingOverlay('Creating project...');
const res = await fetch('/claude/api/claude/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
metadata: {
type: 'chat',
source: 'web-ide',
project: projectName
}
})
});
if (!res.ok) throw new Error('Request failed');
const data = await res.json();
if (data.success) {
// Minimum display time for smooth UX
await new Promise(resolve => setTimeout(resolve, 300));
window.location.href = `/claude/ide?session=${data.session.id}`;
}
} catch (error) {
console.error('Error creating project:', error);
hideLoadingOverlay();
showToast('Failed to create project', 'error');
}
}
/**
* Load all sessions and render in table
*/
async function loadSessions() {
const tbody = document.getElementById('projects-tbody');
const emptyState = document.getElementById('projects-empty');
const table = document.getElementById('projects-table');
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; padding: 40px;">Loading...</td></tr>';
try {
const res = await fetch('/claude/api/claude/sessions');
if (!res.ok) throw new Error('Request failed');
const data = await res.json();
const allSessions = [
...(data.active || []).map(s => ({...s, type: 'active'})),
...(data.historical || []).map(s => ({...s, type: 'historical'}))
];
renderProjectsTable(allSessions);
} catch (error) {
console.error('Error loading sessions:', error);
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: #ff6b6b;">Failed to load</td></tr>';
}
}
/**
* Render projects table with session data
*/
function renderProjectsTable(sessions) {
const tbody = document.getElementById('projects-tbody');
const emptyState = document.getElementById('projects-empty');
const table = document.getElementById('projects-table');
if (!tbody) return;
// 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;
});
tbody.innerHTML = '';
if (sessions.length === 0) {
if (table) table.style.display = 'none';
if (emptyState) emptyState.style.display = 'block';
return;
}
if (table) table.style.display = 'table';
if (emptyState) emptyState.style.display = 'none';
sessions.forEach(session => {
const row = createProjectRow(session);
tbody.appendChild(row);
});
}
/**
* Create a single project row for the table
*/
function createProjectRow(session) {
const tr = document.createElement('tr');
tr.dataset.sessionId = session.id;
const projectName = getProjectName(session);
const relativeTime = getRelativeTime(session);
const status = getStatus(session);
const statusClass = getStatusClass(session);
tr.innerHTML = `
<td data-label="Project">
<h3 class="project-name">${escapeHtml(projectName)}</h3>
</td>
<td data-label="Last Activity">
<span class="last-activity">${relativeTime}</span>
</td>
<td data-label="Status">
<span class="status-badge ${statusClass}">${status}</span>
</td>
<td data-label="Actions">
<button class="btn-continue">Continue</button>
<button class="btn-menu" aria-label="Menu">⋮</button>
</td>
`;
tr.addEventListener('click', (e) => {
if (!e.target.closest('.btn-menu')) {
continueToSession(session.id);
}
});
tr.querySelector('.btn-continue').addEventListener('click', (e) => {
e.stopPropagation();
continueToSession(session.id);
});
tr.querySelector('.btn-menu').addEventListener('click', (e) => {
e.stopPropagation();
showProjectMenu(e, session);
});
return tr;
}
/**
* Extract project name from session metadata
*/
function getProjectName(session) {
return session.metadata?.project ||
session.metadata?.projectName ||
session.workingDir?.split('/').pop() ||
'Session ' + session.id.substring(0, 8);
}
/**
* Get relative time string for session
*/
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();
}
/**
* Get status text for session
*/
function getStatus(session) {
if (session.status === 'running') return 'Active';
return 'Done';
}
/**
* Get status CSS class for session
*/
function getStatusClass(session) {
if (session.status === 'running') return 'active';
return 'done';
}
/**
* Navigate to IDE with session
*/
async function continueToSession(sessionId) {
showLoadingOverlay('Opening workspace...');
await new Promise(resolve => setTimeout(resolve, 300));
window.location.href = `/claude/ide?session=${sessionId}`;
}
/**
* Show project menu dropdown
*/
function showProjectMenu(event, session) {
closeProjectMenu();
const menu = document.createElement('div');
menu.className = 'dropdown-menu';
menu.id = 'project-menu';
menu.innerHTML = `
<button class="dropdown-menu-item" data-action="rename">✏️ Rename</button>
<button class="dropdown-menu-item" data-action="duplicate">📋 Duplicate</button>
<button class="dropdown-menu-item danger" data-action="delete">🗑️ Delete</button>
`;
const rect = event.target.getBoundingClientRect();
menu.style.position = 'fixed';
menu.style.top = `${rect.bottom + 8}px`;
menu.style.right = `${window.innerWidth - rect.right}px`;
document.body.appendChild(menu);
menu.querySelector('[data-action="rename"]').addEventListener('click', () => {
closeProjectMenu();
renameProject(session);
});
menu.querySelector('[data-action="duplicate"]').addEventListener('click', async () => {
closeProjectMenu();
await duplicateProject(session);
loadSessions();
});
menu.querySelector('[data-action="delete"]').addEventListener('click', async () => {
closeProjectMenu();
await deleteProject(session);
loadSessions();
});
setTimeout(() => {
document.addEventListener('click', closeProjectMenu, { once: true });
}, 10);
}
/**
* Close project menu dropdown
*/
function closeProjectMenu() {
document.getElementById('project_menu')?.remove();
}
/**
* Rename project (inline edit)
*/
function renameProject(session) {
const row = document.querySelector(`tr[data-session-id="${session.id}"]`);
if (!row) return;
const nameCell = row.querySelector('.project-name');
if (!nameCell) return;
const currentName = nameCell.textContent;
// Only block truly historical sessions (loaded from disk, not in memory)
// Active and terminated sessions in memory can be renamed
if (session.type === 'historical') {
showToast('Cannot rename historical sessions', 'warning');
return;
}
// Create input element
const input = document.createElement('input');
input.type = 'text';
input.className = 'project-name-input';
input.value = currentName;
input.maxLength = 50;
// Replace text with input
nameCell.innerHTML = '';
nameCell.appendChild(input);
input.focus();
input.select();
// Handle save
const saveRename = async () => {
const newName = input.value.trim();
// Validation
if (!newName) {
nameCell.textContent = currentName;
showToast('Project name cannot be empty', 'error');
return;
}
if (!validateProjectName(newName)) {
nameCell.textContent = currentName;
showToast('Project name contains invalid characters', 'error');
return;
}
if (newName === currentName) {
nameCell.textContent = currentName;
return;
}
// Show saving state
input.disabled = true;
input.style.opacity = '0.6';
try {
const res = await fetch(`/claude/api/claude/sessions/${session.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
metadata: {
...session.metadata,
project: newName
}
})
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || 'Failed to rename');
}
const data = await res.json();
if (data.success) {
nameCell.textContent = newName;
showToast('Project renamed successfully', 'success');
} else {
throw new Error(data.error || 'Failed to rename');
}
} catch (error) {
console.error('Error renaming project:', error);
nameCell.textContent = currentName;
showToast(error.message || 'Failed to rename project', 'error');
}
};
// Handle cancel
const cancelRename = () => {
nameCell.textContent = currentName;
};
// Event listeners
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
input.blur(); // Trigger save on blur
} else if (e.key === 'Escape') {
cancelRename();
}
});
input.addEventListener('blur', () => {
// Small delay to allow Enter key to process first
setTimeout(() => {
if (document.activeElement !== input) {
saveRename();
}
}, 100);
});
}
/**
* Duplicate project/session
*/
async function duplicateProject(session) {
try {
showLoadingOverlay('Duplicating project...');
const res = await fetch(`/claude/api/claude/sessions/${session.id}/duplicate`, {
method: 'POST'
});
if (!res.ok) throw new Error('Request failed');
const data = await res.json();
if (data.success) {
hideLoadingOverlay();
showToast('Project duplicated successfully', 'success');
} else {
throw new Error(data.error || 'Failed to duplicate');
}
} catch (error) {
console.error('Error duplicating project:', error);
hideLoadingOverlay();
showToast('Failed to duplicate project', 'error');
}
}
/**
* Delete project/session with confirmation
*/
async function deleteProject(session) {
const projectName = getProjectName(session);
if (!confirm(`Are you sure you want to delete "${projectName}"? This action cannot be undone.`)) {
return;
}
try {
showLoadingOverlay('Deleting project...');
const res = await fetch(`/claude/api/claude/sessions/${session.id}`, {
method: 'DELETE'
});
if (!res.ok) throw new Error('Request failed');
const data = await res.json();
if (data.success) {
hideLoadingOverlay();
showToast('Project deleted successfully', 'success');
} else {
throw new Error(data.error || 'Failed to delete');
}
} catch (error) {
console.error('Error deleting project:', error);
hideLoadingOverlay();
showToast('Failed to delete project', 'error');
}
}
// Refresh sessions
function refreshSessions() {
loadSessions();
}
/**
* Validate project name for invalid characters
* @param {string} name - The project name to validate
* @returns {boolean} - True if valid, false if contains invalid characters
*/
function validateProjectName(name) {
const invalidChars = /[\/\\<>:"|?*]/;
return !invalidChars.test(name);
}
/**
* Show loading overlay
* @param {string} message - Optional custom message (default: "Loading...")
*/
function showLoadingOverlay(message = 'Loading...') {
let overlay = document.getElementById('loading-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'loading-overlay';
overlay.className = 'loading-overlay';
overlay.innerHTML = `
<div class="loading-spinner"></div>
<p class="loading-text">${escapeHtml(message)}</p>
`;
document.body.appendChild(overlay);
} else {
// Update message if provided
const textElement = overlay.querySelector('.loading-text');
if (textElement) {
textElement.textContent = message;
}
}
overlay.classList.remove('hidden');
setTimeout(() => {
overlay.classList.add('visible');
}, 10);
}
/**
* Hide loading overlay
*/
function hideLoadingOverlay() {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
overlay.classList.remove('visible');
setTimeout(() => {
overlay.classList.add('hidden');
}, 300);
}
}
/**
* Show toast notification
* @param {string} message - The message to display
* @param {string} type - The type of toast: 'success', 'error', 'info'
* @param {number} duration - Duration in milliseconds (default: 3000)
*/
function showToast(message, type = 'info', duration = 3000) {
// Remove existing toasts
const existingToasts = document.querySelectorAll('.toast-notification');
existingToasts.forEach(toast => toast.remove());
// Create toast element
const toast = document.createElement('div');
toast.className = `toast-notification toast-${type}`;
toast.innerHTML = `
<span class="toast-icon">${getToastIcon(type)}</span>
<span class="toast-message">${escapeHtml(message)}</span>
`;
document.body.appendChild(toast);
// Trigger animation
setTimeout(() => {
toast.classList.add('visible');
}, 10);
// Auto remove after duration
setTimeout(() => {
toast.classList.remove('visible');
setTimeout(() => {
toast.remove();
}, 300);
}, duration);
}
/**
* Get toast icon based on type
*/
function getToastIcon(type) {
const icons = {
success: '✓',
error: '✕',
info: '',
warning: '⚠'
};
return icons[type] || icons.info;
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}