Bug fixes: - Add missing showLoadingOverlay/hideLoadingOverlay functions to ide.js (previously only existed in sessions-landing.js, causing continueSessionInChat to fail) - Add loading overlay CSS styles to main style.css - Fix Projects button URL: /projects -> /claude/ide?view=projects - Add ?view= URL parameter handling in ide.js initialization - Add missing Native mode button to chat view (now has 3 modes: Chat, Native, Terminal) These fixes resolve: 1. "Continue in Chat" button not working in sessions view 2. Projects button in landing page nav taking to wrong URL 3. Missing "Native" mode button (user referred to as "Full Stack mode") 4. Loading overlay not displaying in IDE Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1155 lines
39 KiB
JavaScript
1155 lines
39 KiB
JavaScript
// Claude Code IDE JavaScript
|
||
let currentSession = null;
|
||
let currentProjectName = null;
|
||
let ws = null;
|
||
|
||
// Make ws globally accessible for other scripts
|
||
Object.defineProperty(window, 'ws', {
|
||
get: function() { return ws; },
|
||
set: function(value) { ws = value; },
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initNavigation();
|
||
connectWebSocket();
|
||
|
||
// Check URL params for session, prompt, project, and view
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const sessionId = urlParams.get('session');
|
||
const prompt = urlParams.get('prompt');
|
||
const project = urlParams.get('project');
|
||
const view = urlParams.get('view');
|
||
|
||
// Parse project parameter if present
|
||
if (project) {
|
||
window.currentProjectDir = decodeURIComponent(project);
|
||
currentProjectName = project.split('/').filter(Boolean).pop() || 'Project';
|
||
console.log('[Init] Project context loaded:', currentProjectName, window.currentProjectDir);
|
||
}
|
||
|
||
if (sessionId || prompt) {
|
||
// Switch to chat view first
|
||
switchView('chat');
|
||
|
||
// Wait for chat to load, then handle session/prompt
|
||
setTimeout(() => {
|
||
if (sessionId) {
|
||
attachToSession(sessionId);
|
||
}
|
||
|
||
if (prompt) {
|
||
setTimeout(() => {
|
||
const input = document.getElementById('chat-input');
|
||
if (input) {
|
||
input.value = decodeURIComponent(prompt);
|
||
sendChatMessage();
|
||
}
|
||
}, 1000);
|
||
}
|
||
}, 500);
|
||
} else if (view) {
|
||
// Switch to the specified view
|
||
switchView(view);
|
||
} else {
|
||
// Default to chat view
|
||
switchView('chat');
|
||
}
|
||
});
|
||
|
||
// Navigation
|
||
function initNavigation() {
|
||
const navItems = document.querySelectorAll('.nav-item');
|
||
navItems.forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
const view = item.dataset.view;
|
||
switchView(view);
|
||
});
|
||
});
|
||
}
|
||
|
||
function switchView(viewName) {
|
||
// Update nav items
|
||
document.querySelectorAll('.nav-item').forEach(item => {
|
||
item.classList.remove('active');
|
||
if (item.dataset.view === viewName) {
|
||
item.classList.add('active');
|
||
}
|
||
});
|
||
|
||
// Update views
|
||
document.querySelectorAll('.view').forEach(view => {
|
||
view.classList.remove('active');
|
||
});
|
||
document.getElementById(`${viewName}-view`).classList.add('active');
|
||
|
||
// Load content for the view
|
||
switch(viewName) {
|
||
case 'dashboard':
|
||
loadDashboard();
|
||
break;
|
||
case 'chat':
|
||
loadChatView();
|
||
break;
|
||
case 'sessions':
|
||
loadSessions();
|
||
break;
|
||
case 'projects':
|
||
loadProjects();
|
||
break;
|
||
case 'files':
|
||
loadFiles();
|
||
break;
|
||
case 'terminal':
|
||
loadTerminal();
|
||
break;
|
||
}
|
||
}
|
||
|
||
// WebSocket Connection
|
||
function connectWebSocket() {
|
||
const wsUrl = `wss://${window.location.host}/claude/api/claude/chat`;
|
||
console.log('Connecting to WebSocket:', wsUrl);
|
||
|
||
window.ws = new WebSocket(wsUrl);
|
||
|
||
window.ws.onopen = () => {
|
||
console.log('WebSocket connected, readyState:', window.ws.readyState);
|
||
// Send a test message to verify connection
|
||
try {
|
||
window.ws.send(JSON.stringify({ type: 'ping' }));
|
||
} catch (error) {
|
||
console.error('Error sending ping:', error);
|
||
}
|
||
};
|
||
|
||
window.ws.onmessage = (event) => {
|
||
const data = JSON.parse(event.data);
|
||
console.log('WebSocket message received:', data.type);
|
||
handleWebSocketMessage(data);
|
||
};
|
||
|
||
window.ws.onerror = (error) => {
|
||
console.error('WebSocket error:', error);
|
||
console.log('WebSocket error details:', {
|
||
type: error.type,
|
||
target: error.target,
|
||
readyState: window.ws?.readyState
|
||
});
|
||
};
|
||
|
||
window.ws.onclose = (event) => {
|
||
console.log('WebSocket disconnected:', {
|
||
code: event.code,
|
||
reason: event.reason,
|
||
wasClean: event.wasClean
|
||
});
|
||
// Clear the ws reference
|
||
window.ws = null;
|
||
// Attempt to reconnect after 5 seconds
|
||
setTimeout(() => {
|
||
console.log('Attempting to reconnect...');
|
||
connectWebSocket();
|
||
}, 5000);
|
||
};
|
||
}
|
||
|
||
function handleWebSocketMessage(data) {
|
||
switch(data.type) {
|
||
case 'connected':
|
||
console.log(data.message);
|
||
break;
|
||
case 'output':
|
||
handleSessionOutput(data);
|
||
break;
|
||
case 'operations-detected':
|
||
handleOperationsDetected(data);
|
||
break;
|
||
case 'operations-executed':
|
||
handleOperationsExecuted(data);
|
||
break;
|
||
case 'operations-error':
|
||
handleOperationsError(data);
|
||
break;
|
||
case 'operation-progress':
|
||
handleOperationProgress(data);
|
||
break;
|
||
case 'error':
|
||
console.error('WebSocket error:', data.error);
|
||
// Show error in chat if attached
|
||
if (typeof appendSystemMessage === 'function') {
|
||
appendSystemMessage('Error: ' + data.error);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle operations detected event
|
||
*/
|
||
function handleOperationsDetected(data) {
|
||
console.log('Operations detected:', data.operations.length);
|
||
|
||
// Only show if we're attached to this session
|
||
if (data.sessionId !== attachedSessionId) return;
|
||
|
||
// Store response for execution
|
||
window.currentOperationsResponse = data.response;
|
||
|
||
// Use tag renderer to show operations panel
|
||
if (typeof tagRenderer !== 'undefined') {
|
||
tagRenderer.showOperationsPanel(data.operations, data.response);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle operations executed event
|
||
*/
|
||
function handleOperationsExecuted(data) {
|
||
console.log('Operations executed:', data.results);
|
||
|
||
// Only handle if we're attached to this session
|
||
if (data.sessionId !== attachedSessionId) return;
|
||
|
||
// Hide progress and show completion
|
||
if (typeof tagRenderer !== 'undefined') {
|
||
tagRenderer.hideProgress();
|
||
tagRenderer.hideOperationsPanel();
|
||
tagRenderer.showCompletion(data.results);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle operations error event
|
||
*/
|
||
function handleOperationsError(data) {
|
||
console.error('Operations error:', data.error);
|
||
|
||
// Only handle if we're attached to this session
|
||
if (data.sessionId !== attachedSessionId) return;
|
||
|
||
// Show error
|
||
if (typeof tagRenderer !== 'undefined') {
|
||
tagRenderer.hideProgress();
|
||
tagRenderer.showError(data.error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle operation progress event
|
||
*/
|
||
function handleOperationProgress(data) {
|
||
console.log('Operation progress:', data.progress);
|
||
|
||
// Only handle if we're attached to this session
|
||
if (data.sessionId !== attachedSessionId) return;
|
||
|
||
// Update progress
|
||
if (typeof tagRenderer !== 'undefined') {
|
||
const progress = data.progress;
|
||
let message = '';
|
||
|
||
switch(progress.type) {
|
||
case 'write':
|
||
message = `Creating ${progress.path}...`;
|
||
break;
|
||
case 'rename':
|
||
message = `Renaming ${progress.from} to ${progress.to}...`;
|
||
break;
|
||
case 'delete':
|
||
message = `Deleting ${progress.path}...`;
|
||
break;
|
||
case 'install':
|
||
message = `Installing packages: ${progress.packages.join(', ')}...`;
|
||
break;
|
||
case 'command':
|
||
message = `Executing command: ${progress.command}...`;
|
||
break;
|
||
default:
|
||
message = 'Processing...';
|
||
}
|
||
|
||
tagRenderer.updateProgress(message);
|
||
}
|
||
}
|
||
|
||
function handleSessionOutput(data) {
|
||
// Handle output for sessions view
|
||
if (currentSession && data.sessionId === currentSession.id) {
|
||
appendOutput(data.data);
|
||
}
|
||
|
||
// Handle output for chat view
|
||
if (typeof attachedSessionId !== 'undefined' && data.sessionId === attachedSessionId) {
|
||
// Hide streaming indicator
|
||
if (typeof hideStreamingIndicator === 'function') {
|
||
hideStreamingIndicator();
|
||
}
|
||
|
||
// Append output as assistant message
|
||
if (typeof appendMessage === 'function') {
|
||
appendMessage('assistant', data.data.content, true);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Dashboard
|
||
async function loadDashboard() {
|
||
try {
|
||
// Load stats
|
||
const [sessionsRes, projectsRes] = await Promise.all([
|
||
fetch('/claude/api/claude/sessions'),
|
||
fetch('/claude/api/claude/projects')
|
||
]);
|
||
|
||
const sessionsData = await sessionsRes.json();
|
||
const projectsData = await projectsRes.json();
|
||
|
||
// Update stats
|
||
document.getElementById('active-sessions-count').textContent =
|
||
sessionsData.active?.length || 0;
|
||
document.getElementById('historical-sessions-count').textContent =
|
||
sessionsData.historical?.length || 0;
|
||
document.getElementById('total-projects-count').textContent =
|
||
projectsData.projects?.length || 0;
|
||
|
||
// Update active sessions list
|
||
const activeSessionsEl = document.getElementById('active-sessions-list');
|
||
if (sessionsData.active && sessionsData.active.length > 0) {
|
||
activeSessionsEl.innerHTML = sessionsData.active.map(session => `
|
||
<div class="session-item" onclick="viewSession('${session.id}')">
|
||
<div class="session-header">
|
||
<span class="session-id">${session.id.substring(0, 20)}...</span>
|
||
<span class="session-status ${session.status}">${session.status}</span>
|
||
</div>
|
||
<div class="session-meta">
|
||
Working: ${session.workingDir}<br>
|
||
Created: ${new Date(session.createdAt).toLocaleString()}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
activeSessionsEl.innerHTML = '<p class="placeholder">No active sessions</p>';
|
||
}
|
||
|
||
// Update projects list
|
||
const projectsEl = document.getElementById('recent-projects-list');
|
||
if (projectsData.projects && projectsData.projects.length > 0) {
|
||
projectsEl.innerHTML = projectsData.projects.slice(0, 5).map(project => `
|
||
<div class="project-card" onclick="viewProject('${project.name}')">
|
||
<h3>${project.name}</h3>
|
||
<p class="project-meta">
|
||
Modified: ${new Date(project.modified).toLocaleDateString()}
|
||
</p>
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
projectsEl.innerHTML = '<p class="placeholder">No projects yet</p>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading dashboard:', error);
|
||
}
|
||
}
|
||
|
||
function refreshSessions() {
|
||
loadDashboard();
|
||
}
|
||
|
||
// Sessions
|
||
async function loadSessions() {
|
||
const sessionsListEl = document.getElementById('sessions-list');
|
||
|
||
try {
|
||
// Get current project from URL
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const projectPath = urlParams.get('project');
|
||
|
||
// Build API URL with project filter
|
||
let apiUrl = '/claude/api/claude/sessions';
|
||
if (projectPath) {
|
||
apiUrl += `?project=${encodeURIComponent(projectPath)}`;
|
||
console.log('[Sessions] Loading sessions for project:', projectPath);
|
||
}
|
||
|
||
// Show loading state
|
||
sessionsListEl.innerHTML = '<div class="loading">Loading sessions...</div>';
|
||
|
||
const res = await fetch(apiUrl);
|
||
|
||
// Handle HTTP errors
|
||
if (!res.ok) {
|
||
if (res.status === 401) {
|
||
sessionsListEl.innerHTML = `
|
||
<div class="error-state">
|
||
<p>⚠️ Session expired</p>
|
||
<button class="btn-primary" onclick="location.reload()">Login Again</button>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
|
||
// Handle API errors
|
||
if (data.error) {
|
||
throw new Error(data.error);
|
||
}
|
||
|
||
const allSessions = [
|
||
...(data.active || []).map(s => ({...s, type: 'active'})),
|
||
...(data.historical || []).map(s => ({...s, type: 'historical'}))
|
||
];
|
||
|
||
// Sort by last activity (newest first)
|
||
allSessions.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;
|
||
});
|
||
|
||
// Empty state
|
||
if (allSessions.length === 0) {
|
||
const projectName = projectPath ? projectPath.split('/').pop() : 'this project';
|
||
sessionsListEl.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-icon">📂</div>
|
||
<p>No sessions found for <strong>${escapeHtml(projectName)}</strong></p>
|
||
<button class="btn-primary" onclick="startNewChat()">Create New Session</button>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// Render session list
|
||
sessionsListEl.innerHTML = allSessions.map(session => {
|
||
const isRunning = session.status === 'running' && session.type === 'active';
|
||
const relativeTime = getRelativeTime(session);
|
||
const messageCount = session.messageCount || session.metadata?.messageCount || 0;
|
||
|
||
return `
|
||
<div class="session-item ${session.type}" onclick="viewSessionDetails('${session.id}')">
|
||
<div class="session-header">
|
||
<div class="session-info">
|
||
<span class="session-id">${session.id.substring(0, 12)}...</span>
|
||
<span class="session-status ${isRunning ? 'running' : 'stopped'}">
|
||
${isRunning ? '🟢 Running' : '⏸️ ' + (session.type === 'historical' ? 'Historical' : 'Stopped')}
|
||
</span>
|
||
</div>
|
||
<div class="session-time">${relativeTime}</div>
|
||
</div>
|
||
<div class="session-meta">
|
||
<div class="session-path">📁 ${escapeHtml(session.workingDir)}</div>
|
||
<div class="session-stats">
|
||
<span>💬 ${messageCount} messages</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
} catch (error) {
|
||
console.error('[loadSessions] Error:', error);
|
||
sessionsListEl.innerHTML = `
|
||
<div class="error-state">
|
||
<div class="error-icon">⚠️</div>
|
||
<p>Failed to load sessions</p>
|
||
<p class="error-message">${escapeHtml(error.message)}</p>
|
||
<button class="btn-secondary" onclick="loadSessions()">Try Again</button>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
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);
|
||
|
||
if (diffMins < 1) return 'Just now';
|
||
if (diffMins < 60) return `${diffMins}m ago`;
|
||
if (diffMins < 1440) return `${Math.floor(diffMins/60)}h ago`;
|
||
return `${Math.floor(diffMins/1440)}d ago`;
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
async function viewSessionDetails(sessionId) {
|
||
const detailEl = document.getElementById('session-detail');
|
||
|
||
try {
|
||
// Show loading state
|
||
detailEl.innerHTML = '<div class="loading">Loading session details...</div>';
|
||
|
||
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`);
|
||
|
||
// Handle 404 - session not found
|
||
if (res.status === 404) {
|
||
detailEl.innerHTML = `
|
||
<div class="error-state">
|
||
<div class="error-icon">🔍</div>
|
||
<h3>Session Not Found</h3>
|
||
<p>The session <code>${escapeHtml(sessionId)}</code> could not be found.</p>
|
||
<button class="btn-primary" onclick="loadSessions()">Back to Sessions</button>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
if (!res.ok) {
|
||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.error) {
|
||
throw new Error(data.error);
|
||
}
|
||
|
||
if (!data.session) {
|
||
throw new Error('No session data in response');
|
||
}
|
||
|
||
const session = data.session;
|
||
const isRunning = session.status === 'running' && session.pid;
|
||
const messageCount = session.outputBuffer?.length || 0;
|
||
|
||
// Render session detail card
|
||
detailEl.innerHTML = `
|
||
<div class="session-detail-card">
|
||
<div class="session-detail-header">
|
||
<div class="session-title">
|
||
<h2>Session ${session.id.substring(0, 12)}...</h2>
|
||
<span class="session-status-badge ${isRunning ? 'running' : 'stopped'}">
|
||
${isRunning ? '🟢 Running' : '⏸️ Stopped'}
|
||
</span>
|
||
</div>
|
||
<div class="session-detail-actions">
|
||
<button class="btn-primary" onclick="continueSessionInChat('${session.id}')">
|
||
💬 Continue in Chat
|
||
</button>
|
||
<button class="btn-secondary" onclick="duplicateSession('${session.id}')">
|
||
📋 Duplicate
|
||
</button>
|
||
${isRunning ? `
|
||
<button class="btn-danger" onclick="terminateSession('${session.id}')">
|
||
⏹️ Terminate
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="session-detail-meta">
|
||
<div class="meta-row">
|
||
<span class="meta-label">Working Directory:</span>
|
||
<span class="meta-value">${escapeHtml(session.workingDir)}</span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Created:</span>
|
||
<span class="meta-value">${new Date(session.createdAt).toLocaleString()}</span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Last Activity:</span>
|
||
<span class="meta-value">${new Date(session.lastActivity).toLocaleString()}</span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Messages:</span>
|
||
<span class="meta-value">${messageCount}</span>
|
||
</div>
|
||
${session.pid ? `
|
||
<div class="meta-row">
|
||
<span class="meta-label">PID:</span>
|
||
<span class="meta-value">${session.pid}</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
|
||
<div class="session-context">
|
||
<h3>Token Usage</h3>
|
||
<div class="context-bar">
|
||
<div class="context-fill" style="width: ${Math.min(100, (session.context?.totalTokens || 0) / (session.context?.maxTokens || 200000) * 100)}%"></div>
|
||
</div>
|
||
<div class="context-stats">
|
||
<span>${(session.context?.totalTokens || 0).toLocaleString()} / ${(session.context?.maxTokens || 200000).toLocaleString()} tokens</span>
|
||
<span>${Math.round((session.context?.totalTokens || 0) / (session.context?.maxTokens || 200000) * 100)}% used</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="session-output-preview">
|
||
<h3>Session Output (${messageCount} entries)</h3>
|
||
<div class="output-scroll-area">
|
||
${session.outputBuffer?.slice(0, 50).map(entry => `
|
||
<div class="output-entry ${entry.type}">
|
||
<div class="output-header">
|
||
<span class="output-type">${entry.type}</span>
|
||
<span class="output-time">${new Date(entry.timestamp).toLocaleTimeString()}</span>
|
||
</div>
|
||
<div class="output-content">${escapeHtml(entry.content.substring(0, 500))}${entry.content.length > 500 ? '...' : ''}</div>
|
||
</div>
|
||
`).join('') || '<p class="no-output">No output yet</p>'}
|
||
${session.outputBuffer?.length > 50 ? `<p class="output-truncated">...and ${session.outputBuffer.length - 50} more entries</p>` : ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
currentSession = session;
|
||
|
||
} catch (error) {
|
||
console.error('[viewSessionDetails] Error:', error);
|
||
detailEl.innerHTML = `
|
||
<div class="error-state">
|
||
<div class="error-icon">⚠️</div>
|
||
<h3>Failed to Load Session</h3>
|
||
<p class="error-message">${escapeHtml(error.message)}</p>
|
||
<button class="btn-primary" onclick="loadSessions()">Back to Sessions</button>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
async function continueSessionInChat(sessionId) {
|
||
console.log('[Sessions] Continuing session in Chat:', sessionId);
|
||
|
||
try {
|
||
showLoadingOverlay('Loading session...');
|
||
|
||
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`);
|
||
if (!res.ok) {
|
||
throw new Error(`HTTP ${res.status}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
if (!data.session) {
|
||
throw new Error('Session not found');
|
||
}
|
||
|
||
const session = data.session;
|
||
|
||
// Check if session is runnable
|
||
if (session.status === 'terminated' || session.status === 'stopped') {
|
||
hideLoadingOverlay();
|
||
|
||
if (confirm('This session has ended. Do you want to create a new session with the same working directory?')) {
|
||
await duplicateSession(sessionId);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Store pending session and switch views
|
||
window.pendingSessionId = sessionId;
|
||
window.pendingSessionData = session;
|
||
|
||
switchView('chat');
|
||
|
||
} catch (error) {
|
||
console.error('[continueSessionInChat] Error:', error);
|
||
hideLoadingOverlay();
|
||
showToast('❌ Failed to load session: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function duplicateSession(sessionId) {
|
||
try {
|
||
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`);
|
||
const data = await res.json();
|
||
|
||
if (!data.session) {
|
||
throw new Error('Session not found');
|
||
}
|
||
|
||
const workingDir = data.session.workingDir;
|
||
const projectName = workingDir.split('/').pop();
|
||
|
||
showLoadingOverlay('Duplicating session...');
|
||
|
||
const createRes = await fetch('/claude/api/claude/sessions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
workingDir,
|
||
metadata: {
|
||
type: 'chat',
|
||
source: 'web-ide',
|
||
project: projectName,
|
||
duplicatedFrom: sessionId
|
||
}
|
||
})
|
||
});
|
||
|
||
if (!createRes.ok) {
|
||
throw new Error(`HTTP ${createRes.status}`);
|
||
}
|
||
|
||
const createData = await createRes.json();
|
||
|
||
hideLoadingOverlay();
|
||
showToast('✅ Session duplicated!', 'success');
|
||
|
||
loadSessions();
|
||
|
||
setTimeout(() => {
|
||
if (confirm('Start chatting in the duplicated session?')) {
|
||
continueSessionInChat(createData.session.id);
|
||
}
|
||
}, 500);
|
||
|
||
} catch (error) {
|
||
console.error('[duplicateSession] Error:', error);
|
||
hideLoadingOverlay();
|
||
showToast('Failed to duplicate session: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function terminateSession(sessionId) {
|
||
if (!confirm('Are you sure you want to terminate this session?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showLoadingOverlay('Terminating session...');
|
||
|
||
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error(`HTTP ${res.status}`);
|
||
}
|
||
|
||
hideLoadingOverlay();
|
||
showToast('✅ Session terminated', 'success');
|
||
|
||
loadSessions();
|
||
|
||
if (currentSession && currentSession.id === sessionId) {
|
||
document.getElementById('session-detail').innerHTML = `
|
||
<div class="placeholder">
|
||
<h2>Session Terminated</h2>
|
||
<p>Select another session from the sidebar</p>
|
||
</div>
|
||
`;
|
||
currentSession = null;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('[terminateSession] Error:', error);
|
||
hideLoadingOverlay();
|
||
showToast('Failed to terminate session: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// Projects
|
||
async function loadProjects() {
|
||
try {
|
||
const res = await fetch('/claude/api/claude/projects');
|
||
const data = await res.json();
|
||
|
||
const gridEl = document.getElementById('projects-grid');
|
||
if (data.projects && data.projects.length > 0) {
|
||
gridEl.innerHTML = data.projects.map(project => `
|
||
<div class="project-card" onclick="viewProject('${project.name}')">
|
||
<h3>${project.name}</h3>
|
||
<p class="project-meta">
|
||
Modified: ${new Date(project.modified).toLocaleString()}
|
||
</p>
|
||
<p class="project-description">Click to view project details</p>
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
gridEl.innerHTML = '<p class="placeholder">No projects yet. Create your first project!</p>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading projects:', error);
|
||
}
|
||
}
|
||
|
||
async function viewProject(projectName) {
|
||
// Open the project file in the files view
|
||
const path = `Claude Projects/${projectName}.md`;
|
||
loadFileContent(path);
|
||
switchView('files');
|
||
}
|
||
|
||
// Files
|
||
async function loadFiles() {
|
||
try {
|
||
const res = await fetch('/claude/api/files');
|
||
const data = await res.json();
|
||
|
||
const treeEl = document.getElementById('file-tree');
|
||
treeEl.innerHTML = renderFileTree(data.tree);
|
||
} catch (error) {
|
||
console.error('Error loading files:', error);
|
||
}
|
||
}
|
||
|
||
async function loadTerminal() {
|
||
// Initialize terminal manager if not already done
|
||
if (!window.terminalManager) {
|
||
window.terminalManager = new TerminalManager();
|
||
await window.terminalManager.initialize();
|
||
}
|
||
|
||
// Set up new terminal button
|
||
const btnNewTerminal = document.getElementById('btn-new-terminal');
|
||
if (btnNewTerminal) {
|
||
btnNewTerminal.onclick = () => {
|
||
window.terminalManager.createTerminal();
|
||
};
|
||
}
|
||
}
|
||
|
||
function renderFileTree(tree, level = 0) {
|
||
return tree.map(item => {
|
||
const padding = level * 1 + 0.5;
|
||
const icon = item.type === 'folder' ? '📁' : '📄';
|
||
|
||
if (item.type === 'folder' && item.children) {
|
||
return `
|
||
<div style="padding-left: ${padding}rem">
|
||
<div class="tree-item folder" onclick="toggleFolder(this)">
|
||
<span>${icon}</span>
|
||
<span>${item.name}</span>
|
||
</div>
|
||
<div class="tree-children" style="display: none;">
|
||
${renderFileTree(item.children, level + 1)}
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
return `
|
||
<div style="padding-left: ${padding}rem">
|
||
<div class="tree-item file" onclick="loadFile('${item.path}')">
|
||
<span>${icon}</span>
|
||
<span>${item.name}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}).join('');
|
||
}
|
||
|
||
function toggleFolder(element) {
|
||
const children = element.parentElement.querySelector('.tree-children');
|
||
const icon = element.querySelector('span:first-child');
|
||
|
||
if (children.style.display === 'none') {
|
||
children.style.display = 'block';
|
||
icon.textContent = '📂';
|
||
} else {
|
||
children.style.display = 'none';
|
||
icon.textContent = '📁';
|
||
}
|
||
}
|
||
|
||
async function loadFile(filePath) {
|
||
try {
|
||
const res = await fetch(`/claude/api/file/${encodeURIComponent(filePath)}`);
|
||
const data = await res.json();
|
||
|
||
const isHtmlFile = filePath.toLowerCase().endsWith('.html') || filePath.toLowerCase().endsWith('.htm');
|
||
|
||
const editorEl = document.getElementById('file-editor');
|
||
|
||
if (isHtmlFile) {
|
||
// HTML file - show with preview option
|
||
editorEl.innerHTML = `
|
||
<div class="file-header">
|
||
<h2>${filePath}</h2>
|
||
<div class="file-actions">
|
||
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
|
||
<button class="btn-primary btn-sm" onclick="showHtmlPreview('${filePath}')">👁️ Preview</button>
|
||
</div>
|
||
</div>
|
||
<div class="file-content" id="file-content-view">
|
||
<div class="view-toggle">
|
||
<button class="toggle-btn active" data-view="code" onclick="switchFileView('code')">Code</button>
|
||
<button class="toggle-btn" data-view="preview" onclick="switchFileView('preview')">Preview</button>
|
||
</div>
|
||
<div class="code-view">
|
||
<pre><code class="language-html">${escapeHtml(data.content)}</code></pre>
|
||
</div>
|
||
<div class="preview-view" style="display: none;">
|
||
<iframe id="html-preview-frame" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Store file content for preview
|
||
window.currentFileContent = data.content;
|
||
window.currentFilePath = filePath;
|
||
|
||
// Highlight code
|
||
if (window.hljs) {
|
||
document.querySelectorAll('#file-content-view pre code').forEach((block) => {
|
||
hljs.highlightElement(block);
|
||
});
|
||
}
|
||
} else {
|
||
// Non-HTML file - show as before
|
||
editorEl.innerHTML = `
|
||
<div class="file-header">
|
||
<h2>${filePath}</h2>
|
||
<div class="file-actions">
|
||
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
|
||
</div>
|
||
</div>
|
||
<div class="file-content">
|
||
<div class="markdown-body">${data.html}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading file:', error);
|
||
}
|
||
}
|
||
|
||
async function loadFileContent(filePath) {
|
||
await loadFile(filePath);
|
||
switchView('files');
|
||
}
|
||
|
||
// HTML Preview Functions
|
||
function showHtmlPreview(filePath) {
|
||
switchFileView('preview');
|
||
}
|
||
|
||
function switchFileView(view) {
|
||
const codeView = document.querySelector('.code-view');
|
||
const previewView = document.querySelector('.preview-view');
|
||
const toggleBtns = document.querySelectorAll('.toggle-btn');
|
||
|
||
// Update buttons
|
||
toggleBtns.forEach(btn => {
|
||
btn.classList.remove('active');
|
||
if (btn.dataset.view === view) {
|
||
btn.classList.add('active');
|
||
}
|
||
});
|
||
|
||
// Show/hide views
|
||
if (view === 'code') {
|
||
codeView.style.display = 'block';
|
||
previewView.style.display = 'none';
|
||
} else if (view === 'preview') {
|
||
codeView.style.display = 'none';
|
||
previewView.style.display = 'block';
|
||
|
||
// Load HTML into iframe using blob URL
|
||
const iframe = document.getElementById('html-preview-frame');
|
||
if (iframe && window.currentFileContent) {
|
||
// Create blob URL from HTML content
|
||
const blob = new Blob([window.currentFileContent], { type: 'text/html' });
|
||
const blobUrl = URL.createObjectURL(blob);
|
||
|
||
// Load blob URL in iframe
|
||
iframe.src = blobUrl;
|
||
|
||
// Clean up blob URL when iframe is unloaded
|
||
iframe.onload = () => {
|
||
// Keep the blob URL active while preview is shown
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Modals
|
||
function createNewSession() {
|
||
document.getElementById('modal-overlay').classList.remove('hidden');
|
||
document.getElementById('new-session-modal').classList.remove('hidden');
|
||
}
|
||
|
||
function createNewProject() {
|
||
document.getElementById('modal-overlay').classList.remove('hidden');
|
||
document.getElementById('new-project-modal').classList.remove('hidden');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('modal-overlay').classList.add('hidden');
|
||
document.querySelectorAll('.modal').forEach(modal => {
|
||
modal.classList.add('hidden');
|
||
});
|
||
}
|
||
|
||
async function submitNewSession() {
|
||
const workingDir = document.getElementById('session-working-dir').value;
|
||
const project = document.getElementById('session-project').value;
|
||
|
||
try {
|
||
const res = await fetch('/claude/api/claude/sessions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
workingDir,
|
||
metadata: { project }
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
closeModal();
|
||
viewSession(data.session.id);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating session:', error);
|
||
alert('Failed to create session');
|
||
}
|
||
}
|
||
|
||
async function submitNewProject() {
|
||
const name = document.getElementById('project-name').value;
|
||
const description = document.getElementById('project-description').value;
|
||
const type = document.getElementById('project-type').value;
|
||
|
||
if (!name) {
|
||
alert('Please enter a project name');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/claude/api/claude/projects', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name, description, type })
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
closeModal();
|
||
loadProjects();
|
||
viewProject(name);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating project:', error);
|
||
alert('Failed to create project');
|
||
}
|
||
}
|
||
|
||
// Utility functions
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
/**
|
||
* Show loading overlay
|
||
* @param {string} message - The message to display
|
||
*/
|
||
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;
|
||
}
|
||
|
||
function showProjects() {
|
||
switchView('projects');
|
||
}
|
||
|
||
// Logout
|
||
document.getElementById('logout-btn')?.addEventListener('click', async () => {
|
||
try {
|
||
await fetch('/claude/api/logout', { method: 'POST' });
|
||
window.location.reload();
|
||
} catch (error) {
|
||
console.error('Error logging out:', error);
|
||
}
|
||
});
|