Files
SuperCharged-Claude-Code-Up…/public/claude-ide/sessions-landing.js
uroma 06843e5300 feat: add session context menu for project reassignment
Implemented smart suggestions UI with visual indicators for moving sessions between projects.

Features:
- Right-click context menu on session rows
- Fetches smart project suggestions from API
- Displays top 3 suggestions with match scores and reasons
- Visual indicators: 🎯 (90+), 📂 (50-89), 💡 (10-49)
- "Open in IDE" option for quick navigation
- "Show All Projects" modal for full project list
- "Move to Unassigned" to remove project association
- Smooth animations and hover effects
- Click outside to close menu
- Responsive design for mobile devices

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 17:09:59 +00:00

1055 lines
32 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();
loadSessionsAndProjects();
});
// 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 projects, then render grouped by project
*/
async function loadSessionsAndProjects() {
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 {
// Fetch both sessions and projects in parallel
const [sessionsRes, projectsRes] = await Promise.all([
fetch('/claude/api/claude/sessions'),
fetch('/api/projects')
]);
if (!sessionsRes.ok) throw new Error('Failed to load sessions');
if (!projectsRes.ok) throw new Error('Failed to load projects');
const sessionsData = await sessionsRes.json();
const projectsData = await projectsRes.json();
// Combine active and historical sessions
const allSessions = [
...(sessionsData.active || []).map(s => ({...s, type: 'active'})),
...(sessionsData.historical || []).map(s => ({...s, type: 'historical'}))
];
// Store projects in global map for quick lookup
window.projectsMap = new Map(
(projectsData.projects || []).map(p => [p.id, p])
);
renderSessionsGroupedByProject(allSessions);
} catch (error) {
console.error('Error loading sessions and projects:', error);
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: #ff6b6b;">Failed to load</td></tr>';
}
}
/**
* Load all sessions and render in table (legacy function for backward compatibility)
*/
async function loadSessions() {
await loadSessionsAndProjects();
}
/**
* Render sessions grouped by project
*/
function renderSessionsGroupedByProject(sessions) {
const tbody = document.getElementById('projects-tbody');
const emptyState = document.getElementById('projects-empty');
const table = document.getElementById('projects-table');
if (!tbody) return;
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';
// Group sessions by projectId
const grouped = {
byProject: new Map(), // projectId -> {project, sessions: []}
unassigned: [] // sessions without projectId
};
sessions.forEach(session => {
const projectId = session.metadata?.projectId;
if (projectId && window.projectsMap?.has(projectId)) {
const project = window.projectsMap.get(projectId);
if (!grouped.byProject.has(projectId)) {
grouped.byProject.set(projectId, {
project: project,
sessions: []
});
}
grouped.byProject.get(projectId).sessions.push(session);
} else {
grouped.unassigned.push(session);
}
});
// Sort sessions within each group by last activity
grouped.byProject.forEach(group => {
group.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;
});
});
grouped.unassigned.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;
});
// Render project sections
grouped.byProject.forEach((group, projectId) => {
const section = createProjectSection(group.project, group.sessions);
tbody.appendChild(section);
});
// Render unassigned sessions if any
if (grouped.unassigned.length > 0) {
const section = createUnassignedSection(grouped.unassigned);
tbody.appendChild(section);
}
}
/**
* Render projects table with session data (legacy function for backward compatibility)
*/
function renderProjectsTable(sessions) {
renderSessionsGroupedByProject(sessions);
}
/**
* Create a collapsible project section
*/
function createProjectSection(project, sessions) {
const container = document.createElement('div');
container.className = 'project-section';
container.dataset.projectId = project.id;
// Create section header
const header = document.createElement('div');
header.className = 'project-section-header';
header.onclick = () => toggleProjectSection(project.id);
const icon = project.icon || '📁';
const sessionCount = sessions.length;
header.innerHTML = `
<span class="project-section-toggle">▼</span>
<span class="project-section-icon">${escapeHtml(icon)}</span>
<span class="project-section-name">${escapeHtml(project.name)}</span>
<span class="project-section-count">${sessionCount} session${sessionCount !== 1 ? 's' : ''}</span>
`;
// Create sessions container
const sessionsContainer = document.createElement('div');
sessionsContainer.className = 'project-section-sessions';
// Render sessions within this project
sessions.forEach(session => {
const row = createSessionRow(session);
sessionsContainer.appendChild(row);
});
container.appendChild(header);
container.appendChild(sessionsContainer);
return container;
}
/**
* Create unassigned sessions section
*/
function createUnassignedSection(sessions) {
const container = document.createElement('div');
container.className = 'project-section unassigned-section';
// Create section header
const header = document.createElement('div');
header.className = 'project-section-header';
header.onclick = () => toggleProjectSection('unassigned');
const sessionCount = sessions.length;
header.innerHTML = `
<span class="project-section-toggle">▼</span>
<span class="project-section-icon">📄</span>
<span class="project-section-name">Unassigned Sessions</span>
<span class="project-section-count">${sessionCount} session${sessionCount !== 1 ? 's' : ''}</span>
`;
// Create sessions container
const sessionsContainer = document.createElement('div');
sessionsContainer.className = 'project-section-sessions';
// Render unassigned sessions
sessions.forEach(session => {
const row = createSessionRow(session);
sessionsContainer.appendChild(row);
});
container.appendChild(header);
container.appendChild(sessionsContainer);
return container;
}
/**
* Create a single session row
*/
function createSessionRow(session) {
const tr = document.createElement('tr');
tr.className = 'session-row';
tr.dataset.sessionId = session.id;
const projectName = getProjectName(session);
const relativeTime = getRelativeTime(session);
const status = getStatus(session);
const statusClass = getStatusClass(session);
// Check if session has a project assigned
const projectId = session.metadata?.projectId;
const hasProject = projectId && window.projectsMap?.has(projectId);
const projectBadge = hasProject ?
`<span class="session-project-badge">${escapeHtml(window.projectsMap.get(projectId).name)}</span>` : '';
tr.innerHTML = `
<td data-label="Project">
<h3 class="project-name">${escapeHtml(projectName)}</h3>
${projectBadge}
</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);
});
// Add right-click context menu
tr.addEventListener('contextmenu', (e) => {
e.preventDefault();
showSessionContextMenu(e, session.id);
});
return tr;
}
/**
* Toggle project section collapse/expand
*/
function toggleProjectSection(projectId) {
const section = projectId === 'unassigned' ?
document.querySelector('.unassigned-section') :
document.querySelector(`.project-section[data-project-id="${projectId}"]`);
if (!section) return;
const toggle = section.querySelector('.project-section-toggle');
const sessionsContainer = section.querySelector('.project-section-sessions');
if (section.classList.contains('collapsed')) {
// Expand
section.classList.remove('collapsed');
toggle.textContent = '▼';
sessionsContainer.style.display = '';
} else {
// Collapse
section.classList.add('collapsed');
toggle.textContent = '▶';
sessionsContainer.style.display = 'none';
}
}
/**
* Create a single project row for the table (legacy function for backward compatibility)
*/
function createProjectRow(session) {
return createSessionRow(session);
}
/**
* 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);
loadSessionsAndProjects();
});
menu.querySelector('[data-action="delete"]').addEventListener('click', async () => {
closeProjectMenu();
await deleteProject(session);
loadSessionsAndProjects();
});
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() {
loadSessionsAndProjects();
}
/**
* 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;
}
/**
* Show session context menu for project reassignment
*/
async function showSessionContextMenu(event, sessionId) {
event.preventDefault();
hideSessionContextMenu();
// Create context menu element
const menu = document.createElement('div');
menu.id = 'sessionContextMenu';
menu.className = 'session-context-menu';
// Fetch project suggestions
let suggestions = [];
try {
const res = await fetch(`/api/projects/suggestions?sessionId=${sessionId}`);
if (res.ok) {
const data = await res.json();
suggestions = data.suggestions || [];
}
} catch (error) {
console.error('Error fetching suggestions:', error);
}
// Build menu HTML
let menuHtml = `
<div class="context-menu-item" data-action="open">
<span class="context-menu-icon">🚀</span>
<span class="context-menu-text">Open in IDE</span>
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-label">Move to Project</div>
`;
// Add top 3 suggestions
const topSuggestions = suggestions.slice(0, 3);
topSuggestions.forEach(suggestion => {
const icon = getMatchIcon(suggestion.score);
const project = window.projectsMap?.get(suggestion.projectId);
if (project) {
menuHtml += `
<div class="context-menu-item context-menu-suggestion" data-action="move" data-project-id="${suggestion.projectId}">
<span class="context-menu-icon">${icon}</span>
<div class="context-menu-suggestion-content">
<span class="context-menu-text">${escapeHtml(project.name)}</span>
<span class="context-menu-reason">${escapeHtml(suggestion.reason)}</span>
</div>
<span class="context-menu-score">${Math.round(suggestion.score)}%</span>
</div>
`;
}
});
// Add "Show All Projects" option
menuHtml += `
<div class="context-menu-item" data-action="show-all">
<span class="context-menu-icon">📂</span>
<span class="context-menu-text">Show All Projects...</span>
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" data-action="unassigned">
<span class="context-menu-icon">📄</span>
<span class="context-menu-text">Move to Unassigned</span>
</div>
`;
menu.innerHTML = menuHtml;
document.body.appendChild(menu);
// Position menu at mouse coordinates
const x = event.clientX;
const y = event.clientY;
// Ensure menu doesn't go off screen
const menuRect = menu.getBoundingClientRect();
const maxX = window.innerWidth - menuRect.width - 10;
const maxY = window.innerHeight - menuRect.height - 10;
menu.style.left = `${Math.min(x, maxX)}px`;
menu.style.top = `${Math.min(y, maxY)}px`;
// Store session ID for event handlers
menu.dataset.sessionId = sessionId;
// Add event listeners
menu.addEventListener('click', handleContextMenuClick);
// Close menu when clicking outside
setTimeout(() => {
document.addEventListener('click', hideSessionContextMenu, { once: true });
}, 10);
}
/**
* Handle context menu item clicks
*/
async function handleContextMenuClick(event) {
const menu = document.getElementById('sessionContextMenu');
if (!menu) return;
const target = event.target.closest('.context-menu-item');
if (!target) return;
const action = target.dataset.action;
const sessionId = menu.dataset.sessionId;
hideSessionContextMenu();
switch (action) {
case 'open':
continueToSession(sessionId);
break;
case 'move':
const projectId = target.dataset.projectId;
await moveSessionToProject(sessionId, projectId);
break;
case 'show-all':
showAllProjectsModal(sessionId);
break;
case 'unassigned':
await moveSessionToProject(sessionId, null);
break;
}
}
/**
* Get match icon based on score
*/
function getMatchIcon(score) {
if (score >= 90) return '🎯';
if (score >= 50) return '📂';
return '💡';
}
/**
* Move session to project
*/
async function moveSessionToProject(sessionId, projectId) {
try {
showLoadingOverlay('Moving session...');
const res = await fetch(`/api/projects/sessions/${sessionId}/move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId })
});
if (!res.ok) throw new Error('Request failed');
const data = await res.json();
if (data.success) {
hideLoadingOverlay();
showToast('Session moved successfully', 'success');
await loadSessionsAndProjects();
} else {
throw new Error(data.error || 'Failed to move session');
}
} catch (error) {
console.error('Error moving session:', error);
hideLoadingOverlay();
showToast(error.message || 'Failed to move session', 'error');
}
}
/**
* Show all projects modal
*/
function showAllProjectsModal(sessionId) {
// Create modal overlay
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.id = 'allProjectsModal';
// Create modal content
const modal = document.createElement('div');
modal.className = 'all-projects-modal';
// Build projects list
const projects = Array.from(window.projectsMap?.values() || []);
let projectsHtml = '';
if (projects.length === 0) {
projectsHtml = '<div class="no-projects">No projects available</div>';
} else {
projectsHtml = '<div class="projects-list">';
projects.forEach(project => {
projectsHtml += `
<div class="project-option" data-project-id="${project.id}">
<span class="project-option-icon">${escapeHtml(project.icon || '📁')}</span>
<span class="project-option-name">${escapeHtml(project.name)}</span>
<span class="project-option-arrow">→</span>
</div>
`;
});
projectsHtml += '</div>';
}
modal.innerHTML = `
<div class="modal-header">
<h2>All Projects</h2>
<button class="modal-close" onclick="closeAllProjectsModal()">&times;</button>
</div>
<div class="modal-body">
<p class="modal-info">Select a project to move this session to</p>
${projectsHtml}
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Add click handlers
modal.querySelectorAll('.project-option').forEach(option => {
option.addEventListener('click', async () => {
const projectId = option.dataset.projectId;
closeAllProjectsModal();
await moveSessionToProject(sessionId, projectId);
});
});
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeAllProjectsModal();
}
});
// Trigger animation
setTimeout(() => {
overlay.classList.add('visible');
}, 10);
}
/**
* Close all projects modal
*/
function closeAllProjectsModal() {
const modal = document.getElementById('allProjectsModal');
if (modal) {
modal.classList.remove('visible');
setTimeout(() => {
modal.remove();
}, 300);
}
}
/**
* Hide session context menu
*/
function hideSessionContextMenu() {
const menu = document.getElementById('sessionContextMenu');
if (menu) {
menu.remove();
}
}