- Show actual server error message when project creation fails - Add console logging to debug project creation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
664 lines
19 KiB
JavaScript
664 lines
19 KiB
JavaScript
/**
|
|
* Session Picker Modal
|
|
* Shows when user clicks a project - allows resuming or creating sessions
|
|
* Following CodeNomad's design pattern
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
let currentModal = null;
|
|
let currentProject = null;
|
|
|
|
/**
|
|
* Show session picker modal for a project
|
|
* @param {Object} project - Project object
|
|
*/
|
|
async function showSessionPicker(project) {
|
|
// Close existing modal if open
|
|
closeSessionPicker();
|
|
|
|
currentProject = project;
|
|
|
|
// Create modal overlay
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'modal-overlay';
|
|
overlay.id = 'session-picker-overlay';
|
|
|
|
// Create modal content
|
|
const modal = document.createElement('div');
|
|
modal.className = 'session-picker-modal';
|
|
modal.id = 'session-picker-modal';
|
|
|
|
modal.innerHTML = `
|
|
<div class="session-picker-header">
|
|
<h2 class="session-picker-title">Claude Code • ${escapeHtml(project.name)}</h2>
|
|
<button class="session-picker-close" onclick="window.SessionPicker.close()">×</button>
|
|
</div>
|
|
|
|
<div class="session-picker-content" id="session-picker-content">
|
|
<div class="session-picker-loading">Loading sessions...</div>
|
|
</div>
|
|
|
|
<div class="session-picker-footer">
|
|
<button class="btn-secondary" onclick="window.SessionPicker.close()">Cancel</button>
|
|
</div>
|
|
`;
|
|
|
|
overlay.appendChild(modal);
|
|
document.body.appendChild(overlay);
|
|
|
|
// Prevent body scroll
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
// Trigger animation
|
|
setTimeout(() => {
|
|
overlay.classList.add('visible');
|
|
modal.classList.add('visible');
|
|
}, 10);
|
|
|
|
// Load sessions
|
|
await loadSessionsForProject(project.id);
|
|
|
|
// Close on overlay click
|
|
overlay.addEventListener('click', (e) => {
|
|
if (e.target === overlay) {
|
|
closeSessionPicker();
|
|
}
|
|
});
|
|
|
|
// Close on Escape key
|
|
const escapeHandler = (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeSessionPicker();
|
|
document.removeEventListener('keydown', escapeHandler);
|
|
}
|
|
};
|
|
document.addEventListener('keydown', escapeHandler);
|
|
|
|
currentModal = { overlay, escapeHandler };
|
|
}
|
|
|
|
/**
|
|
* Load sessions for a project
|
|
*/
|
|
async function loadSessionsForProject(projectId) {
|
|
const content = document.getElementById('session-picker-content');
|
|
|
|
try {
|
|
console.log('[SessionPicker] Loading sessions for project:', projectId);
|
|
const res = await fetch(`/api/projects/${projectId}/sessions`, {
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
console.log('[SessionPicker] Response status:', res.status);
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`Failed to load sessions (HTTP ${res.status})`);
|
|
}
|
|
|
|
const data = await res.json();
|
|
console.log('[SessionPicker] Response data:', data);
|
|
|
|
if (data.sessions && data.sessions.length > 0) {
|
|
renderSessionList(data.sessions);
|
|
} else {
|
|
renderEmptyState();
|
|
}
|
|
} catch (error) {
|
|
console.error('[SessionPicker] Error loading sessions:', error);
|
|
console.error('[SessionPicker] Error details:', {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
projectId: projectId
|
|
});
|
|
content.innerHTML = `
|
|
<div class="session-picker-error">
|
|
Failed to load sessions. Please try again.
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render list of sessions
|
|
*/
|
|
function renderSessionList(sessions) {
|
|
const content = document.getElementById('session-picker-content');
|
|
|
|
const sessionsHtml = sessions.map(session => {
|
|
const title = session.title || session.metadata?.project || 'Untitled Session';
|
|
const relativeTime = getRelativeTime(session.updatedAt || session.created_at || session.lastActivity);
|
|
const agent = session.agent || session.metadata?.agent || 'claude';
|
|
|
|
return `
|
|
<button
|
|
class="session-item"
|
|
onclick="window.SessionPicker.resumeSession('${session.id}')"
|
|
>
|
|
<div class="session-item-content">
|
|
<span class="session-item-title">${escapeHtml(title)}</span>
|
|
<span class="session-item-meta">
|
|
<span class="session-agent">${escapeHtml(agent)}</span>
|
|
<span class="session-time">${relativeTime}</span>
|
|
</span>
|
|
</div>
|
|
</button>
|
|
`;
|
|
}).join('');
|
|
|
|
content.innerHTML = `
|
|
<div class="session-section">
|
|
<h3 class="session-section-title">
|
|
Resume a session (${sessions.length}):
|
|
</h3>
|
|
<div class="session-list">
|
|
${sessionsHtml}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="session-divider">
|
|
<span class="session-divider-text">or</span>
|
|
</div>
|
|
|
|
<div class="session-section">
|
|
<h3 class="session-section-title">Start new session:</h3>
|
|
<div class="new-session-form">
|
|
<button
|
|
class="btn-primary"
|
|
onclick="window.SessionPicker.createNewSession()"
|
|
>
|
|
<span class="btn-icon">+</span>
|
|
Create Session
|
|
<kbd class="kbd">⌘↵</kbd>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Render empty state (no existing sessions)
|
|
*/
|
|
function renderEmptyState() {
|
|
const content = document.getElementById('session-picker-content');
|
|
|
|
content.innerHTML = `
|
|
<div class="session-empty-state">
|
|
<div class="empty-state-icon">💬</div>
|
|
<h3 class="empty-state-title">No previous sessions</h3>
|
|
<p class="empty-state-subtitle">Start a new conversation in this project</p>
|
|
</div>
|
|
|
|
<div class="session-divider">
|
|
<span class="session-divider-text">or</span>
|
|
</div>
|
|
|
|
<div class="session-section">
|
|
<h3 class="session-section-title">Start new session:</h3>
|
|
<div class="new-session-form">
|
|
<button
|
|
class="btn-primary"
|
|
onclick="window.SessionPicker.createNewSession()"
|
|
>
|
|
<span class="btn-icon">+</span>
|
|
Create Session
|
|
<kbd class="kbd">⌘↵</kbd>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Resume an existing session
|
|
*/
|
|
async function resumeSession(sessionId) {
|
|
if (!currentProject) return;
|
|
|
|
try {
|
|
showLoadingOverlay('Opening workspace...');
|
|
|
|
// Navigate to IDE with session
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
window.location.href = `/claude/ide?session=${sessionId}`;
|
|
} catch (error) {
|
|
console.error('Error resuming session:', error);
|
|
hideLoadingOverlay();
|
|
showToast('Failed to open session', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new session in the project
|
|
*/
|
|
async function createNewSession() {
|
|
if (!currentProject) return;
|
|
|
|
try {
|
|
showLoadingOverlay('Creating session...');
|
|
|
|
const res = await fetch(`/api/projects/${currentProject.id}/sessions`, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
metadata: {
|
|
type: 'chat',
|
|
source: 'web-ide',
|
|
project: currentProject.name
|
|
}
|
|
})
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error('Failed to create session');
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
if (data.success && data.session) {
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
window.location.href = `/claude/ide?session=${data.session.id}`;
|
|
} else {
|
|
throw new Error(data.error || 'Failed to create session');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating session:', error);
|
|
hideLoadingOverlay();
|
|
showToast('Failed to create session', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close the modal
|
|
*/
|
|
function closeSessionPicker() {
|
|
const overlay = document.getElementById('session-picker-overlay');
|
|
if (!overlay) return;
|
|
|
|
overlay.classList.remove('visible');
|
|
|
|
const modal = document.getElementById('session-picker-modal');
|
|
if (modal) modal.classList.remove('visible');
|
|
|
|
setTimeout(() => {
|
|
if (currentModal && currentModal.escapeHandler) {
|
|
document.removeEventListener('keydown', currentModal.escapeHandler);
|
|
}
|
|
overlay.remove();
|
|
document.body.style.overflow = '';
|
|
currentModal = null;
|
|
currentProject = null;
|
|
}, 300);
|
|
}
|
|
|
|
/**
|
|
* Get relative time string
|
|
*/
|
|
function getRelativeTime(timestamp) {
|
|
const date = new Date(timestamp);
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS
|
|
*/
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Export to global scope
|
|
window.SessionPicker = {
|
|
show: showSessionPicker,
|
|
close: closeSessionPicker,
|
|
resumeSession,
|
|
createNewSession
|
|
};
|
|
|
|
// Add CSS styles
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
/* Modal Overlay */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
backdrop-filter: blur(4px);
|
|
z-index: 10000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.modal-overlay.visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Session Picker Modal */
|
|
.session-picker-modal {
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
border-radius: 16px;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
width: 100%;
|
|
max-width: 600px;
|
|
max-height: 80vh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
transform: scale(0.95);
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.session-picker-modal.visible {
|
|
transform: scale(1);
|
|
}
|
|
|
|
/* Header */
|
|
.session-picker-header {
|
|
padding: 24px;
|
|
border-bottom: 1px solid #333;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.session-picker-title {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #e0e0e0;
|
|
margin: 0;
|
|
}
|
|
|
|
.session-picker-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;
|
|
}
|
|
|
|
.session-picker-close:hover {
|
|
background: #252525;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
/* Content */
|
|
.session-picker-content {
|
|
padding: 24px;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
/* Session Section */
|
|
.session-section {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.session-section-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #888;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin: 0 0 12px 0;
|
|
}
|
|
|
|
/* Session List */
|
|
.session-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.session-item {
|
|
width: 100%;
|
|
text-align: left;
|
|
padding: 16px;
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.session-item:hover {
|
|
background: #252525;
|
|
border-color: #4a9eff;
|
|
}
|
|
|
|
.session-item-content {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.session-item-title {
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
color: #e0e0e0;
|
|
flex: 1;
|
|
}
|
|
|
|
.session-item-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.session-agent {
|
|
font-size: 12px;
|
|
color: #888;
|
|
background: #252525;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.session-time {
|
|
font-size: 12px;
|
|
color: #888;
|
|
}
|
|
|
|
/* Divider */
|
|
.session-divider {
|
|
position: relative;
|
|
margin: 24px 0;
|
|
}
|
|
|
|
.session-divider::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 0;
|
|
right: 0;
|
|
height: 1px;
|
|
background: #333;
|
|
}
|
|
|
|
.session-divider-text {
|
|
position: relative;
|
|
display: block;
|
|
text-align: center;
|
|
font-size: 14px;
|
|
color: #888;
|
|
background: #1a1a1a;
|
|
padding: 0 12px;
|
|
}
|
|
|
|
/* New Session Form */
|
|
.new-session-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* Buttons */
|
|
.btn-primary {
|
|
width: 100%;
|
|
padding: 14px 20px;
|
|
background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%);
|
|
border: none;
|
|
border-radius: 12px;
|
|
color: white;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 20px rgba(74, 158, 255, 0.4);
|
|
}
|
|
|
|
.btn-primary:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.btn-icon {
|
|
font-size: 18px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.btn-secondary {
|
|
padding: 12px 24px;
|
|
background: transparent;
|
|
border: 1px solid #333;
|
|
border-radius: 8px;
|
|
color: #e0e0e0;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #252525;
|
|
border-color: #4a9eff;
|
|
}
|
|
|
|
/* Keyboard Hint */
|
|
.kbd {
|
|
background: #252525;
|
|
border: 1px solid #444;
|
|
border-radius: 6px;
|
|
padding: 4px 8px;
|
|
font-size: 12px;
|
|
font-family: monospace;
|
|
color: #888;
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* Empty State */
|
|
.session-empty-state {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.empty-state-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #e0e0e0;
|
|
margin: 0 0 8px 0;
|
|
}
|
|
|
|
.empty-state-subtitle {
|
|
font-size: 14px;
|
|
color: #888;
|
|
margin: 0;
|
|
}
|
|
|
|
/* Loading State */
|
|
.session-picker-loading {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
color: #888;
|
|
}
|
|
|
|
/* Error State */
|
|
.session-picker-error {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
color: #ff6b6b;
|
|
}
|
|
|
|
/* Footer */
|
|
.session-picker-footer {
|
|
padding: 16px 24px;
|
|
border-top: 1px solid #333;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 640px) {
|
|
.session-picker-modal {
|
|
max-height: 90vh;
|
|
}
|
|
|
|
.session-picker-header,
|
|
.session-picker-content,
|
|
.session-picker-footer {
|
|
padding: 16px;
|
|
}
|
|
|
|
.session-item {
|
|
padding: 12px;
|
|
}
|
|
|
|
.session-item-content {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.session-item-meta {
|
|
width: 100%;
|
|
justify-content: space-between;
|
|
}
|
|
}
|
|
`;
|
|
|
|
document.head.appendChild(style);
|
|
|
|
console.log('[SessionPicker] Module loaded');
|
|
})();
|