// Claude Code IDE JavaScript let currentSession = null; let currentProjectName = null; let ws = null; // Streaming message state for accumulating response chunks // MUST be declared at module scope BEFORE any functions that use them let streamingMessageElement = null; let streamingMessageContent = ''; let streamingTimeout = null; // ============================================================ // REAL-TIME LOGGER WITH AUTO-FIX // ============================================================ // Tier 1: Client-side auto-fix (instant) // Tier 2: Escalation to AI agents (systematic-debugging + brainstorming) window.AutoFixLogger = (function() { const logs = []; const maxLogs = 100; let panel = null; // Create the logger panel function createPanel() { if (panel) return; panel = document.createElement('div'); panel.id = 'autofix-logger-panel'; panel.innerHTML = `
πŸ”§ Auto-Fix Logger
`; // Add styles const style = document.createElement('style'); style.textContent = ` #autofix-logger-panel { position: fixed; bottom: 20px; right: 20px; width: 400px; max-height: 300px; background: #1a1a1a; border: 1px solid #333; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); z-index: 99999; font-family: 'SF Mono', Monaco, monospace; font-size: 12px; overflow: hidden; } #autofix-logger-panel.minimized { height: 40px; } .autofix-logger-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 15px; background: #252525; border-bottom: 1px solid #333; font-weight: 600; } .autofix-logger-header button { background: #333; border: 1px solid #444; color: #e0e0e0; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; margin-left: 5px; } .autofix-logger-header button:hover { background: #444; } .autofix-logger-content { padding: 10px; overflow-y: auto; max-height: 250px; } .autofix-log-entry { margin-bottom: 8px; padding: 8px; border-radius: 6px; border-left: 3px solid #666; } .autofix-log-entry.success { background: rgba(34, 197, 94, 0.1); border-left-color: #22c55e; } .autofix-log-entry.error { background: rgba(239, 68, 68, 0.1); border-left-color: #ef4444; } .autofix-log-entry.warning { background: rgba(234, 179, 8, 0.1); border-left-color: #eab308; } .autofix-log-entry.info { background: rgba(74, 158, 255, 0.1); border-left-color: #4a9eff; } .autofix-log-time { color: #888; font-size: 10px; } .autofix-log-message { margin-top: 4px; color: #e0e0e0; } .autofix-log-detail { margin-top: 4px; padding: 4px 8px; background: #0d0d0d; border-radius: 4px; font-size: 10px; color: #888; } `; document.head.appendChild(style); document.body.appendChild(panel); } function addLog(type, message, detail = null) { const timestamp = new Date().toLocaleTimeString(); const log = { timestamp, type, message, detail }; logs.push(log); if (logs.length > maxLogs) logs.shift(); if (!panel) createPanel(); const content = document.getElementById('autofix-logger-content'); const entry = document.createElement('div'); entry.className = `autofix-log-entry ${type}`; entry.innerHTML = `
${timestamp}
${message}
${detail ? `
${typeof detail === 'object' ? JSON.stringify(detail, null, 2) : detail}
` : ''} `; content.appendChild(entry); content.scrollTop = content.scrollHeight; // Auto-fix triggers checkAndAutoFix(); } function checkAndAutoFix() { const recentLogs = logs.slice(-10); // Detect: Session ID in URL but not attached const hasSessionInUrl = window.PRELOAD_SESSION_ID || window.location.pathname.match(/\/claude\/ide\/session\/([^\/]+)$/); const showsNoSessions = document.body.textContent.includes('No sessions yet'); if (hasSessionInUrl && showsNoSessions) { const sessionId = window.PRELOAD_SESSION_ID || window.location.pathname.match(/\/claude\/ide\/session\/([^\/]+)$/)[1]; // Check if already attached if (typeof attachedSessionId !== 'undefined' && attachedSessionId === sessionId) { return; // Already attached, no fix needed } addLog('warning', '⚠️ Detected: Session in URL but not attached. Attempting auto-fix...'); // Tier 1 Auto-fix: Force attach if (typeof attachToSession === 'function') { setTimeout(() => { attachToSession(sessionId); addLog('success', 'βœ… Auto-fix applied: Force attached to session', { sessionId }); }, 100); } } // Detect: API errors const apiErrors = recentLogs.filter(l => l.type === 'error' && l.message.includes('API')); if (apiErrors.length >= 3) { addLog('warning', '⚠️ Multiple API errors detected. Consider reloading page.'); } } function escalateToAgents(issue) { addLog('warning', 'πŸ€– Tier 1 auto-fix failed. Escalating to AI agents...'); const diagnosticReport = { url: window.location.href, sessionId: window.PRELOAD_SESSION_ID, attachedSessionId: typeof attachedSessionId !== 'undefined' ? attachedSessionId : null, timestamp: new Date().toISOString(), logs: logs.slice(-20), userAgent: navigator.userAgent, sessionStorage: { ...sessionStorage }, localStorage: { ...localStorage } }; addLog('info', 'πŸ“‹ Diagnostic report generated', { issue, reportKeys: Object.keys(diagnosticReport) }); // Store report for agent retrieval sessionStorage.setItem('AUTOFIX_DIAGNOSTIC_REPORT', JSON.stringify(diagnosticReport)); sessionStorage.setItem('AUTOFIX_ISSUE', JSON.stringify(issue)); // The actual agent escalation happens server-side via the skill system console.log('[AUTOFIX] Diagnostic report ready for agent retrieval'); console.log('[AUTOFIX] Report:', diagnosticReport); addLog('info', 'πŸ’‘ Tip: Share this diagnostic report with Claude for agent-assisted fix'); } return { init: function() { createPanel(); addLog('info', 'πŸ”§ Auto-Fix Logger initialized'); addLog('info', 'βœ… PRELOAD_SESSION_ID:', window.PRELOAD_SESSION_ID || 'none'); // Start monitoring setInterval(checkAndAutoFix, 5000); }, log: function(message, detail = null) { addLog('info', message, detail); }, success: function(message, detail = null) { addLog('success', 'βœ… ' + message, detail); }, error: function(message, detail = null) { addLog('error', '❌ ' + message, detail); }, warning: function(message, detail = null) { addLog('warning', '⚠️ ' + message, detail); }, clear: function() { logs.length = 0; if (panel) { document.getElementById('autofix-logger-content').innerHTML = ''; } }, export: function() { const report = { timestamp: new Date().toISOString(), url: window.location.href, sessionId: window.PRELOAD_SESSION_ID, logs: logs }; const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `autofix-log-${Date.now()}.json`; a.click(); }, toggle: function() { if (panel) { panel.classList.toggle('minimized'); } }, escalate: escalateToAgents }; })(); // 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 - Use same pattern as session-picker (run immediately if DOM ready, otherwise wait for DOMContentLoaded) function ideInit() { // ============================================================ // TRACE: ide.js initialized // ============================================================ if (window.traceExecution) { window.traceExecution('ide.js', 'ideInit called', { pathname: window.location.pathname, readyState: document.readyState }); } // ============================================================ // Initialize Auto-Fix Logger FIRST // ============================================================ if (window.AutoFixLogger) { window.AutoFixLogger.init(); } initNavigation(); connectWebSocket(); // ============================================================ // Session ID Extraction - Use PRELOAD_SESSION_ID first // ============================================================ // PRELOAD_SESSION_ID is set by inline script BEFORE any other JS // This guarantees it's available when loadChatView() runs let sessionId = window.PRELOAD_SESSION_ID || null; if (sessionId) { console.log('[Init] Using PRELOAD_SESSION_ID:', sessionId); if (window.AutoFixLogger) { window.AutoFixLogger.success('Session ID from PRELOAD', { sessionId }); } if (window.traceExecution) { window.traceExecution('ide.js', 'Using PRELOAD_SESSION_ID', { sessionId }); } } // Fallback: Extract from route-based URL if PRELOAD didn't work if (!sessionId) { const pathname = window.location.pathname; const sessionMatch = pathname.match(/\/claude\/ide\/session\/([^\/]+)$/); if (sessionMatch && sessionMatch[1]) { sessionId = sessionMatch[1]; console.log('[Init] Extracted sessionId from route:', sessionId); if (window.traceExecution) { window.traceExecution('ide.js', 'Extracted sessionId from URL path', { sessionId, pathname }); } } } // Check URL params for session (legacy format), prompt, project, and view const urlParams = new URLSearchParams(window.location.search); const legacySessionId = urlParams.get('session'); const prompt = urlParams.get('prompt'); const project = urlParams.get('project'); const view = urlParams.get('view'); // Use route-based sessionId if available, otherwise fall back to legacy query param if (!sessionId && legacySessionId) { sessionId = legacySessionId; console.log('[Init] Using legacy sessionId from query param:', sessionId); } // 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) { // CRITICAL: Set pending session attachment flag BEFORE switching view // This allows loadChatView() to know a session is about to be attached // and show appropriate loading state instead of "No sessions yet" if (sessionId) { window.pendingSessionAttach = sessionId; console.log('[Init] Set pendingSessionAttach:', sessionId); if (window.AutoFixLogger) { window.AutoFixLogger.log('Set pendingSessionAttach flag', { sessionId }); } if (window.traceExecution) { window.traceExecution('ide.js', 'Set pendingSessionAttach flag', { sessionId }); } } // Switch to chat view first switchView('chat'); // Wait for chat to load, then handle session/prompt setTimeout(() => { if (sessionId) { if (window.AutoFixLogger) { window.AutoFixLogger.log('Calling attachToSession...', { sessionId }); } if (window.traceExecution) { window.traceExecution('ide.js', 'Calling attachToSession', { 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('chat'); } else { // Default to chat view switchView('chat'); } } // Auto-initialize using same pattern as session-picker // Check if DOM is already loaded, if so run immediately, otherwise wait for DOMContentLoaded if (typeof window !== 'undefined') { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', ideInit); } else { // DOM already loaded, run immediately ideInit(); } } // 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) { if (window.traceExecution) { window.traceExecution('ide.js', 'switchView called', { 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); // Set ready state to connecting window.wsReady = false; window.ws.onopen = () => { console.log('WebSocket connected, readyState:', window.ws.readyState); window.wsReady = true; // Send a test message to verify connection try { window.ws.send(JSON.stringify({ type: 'ping' })); } catch (error) { console.error('Error sending ping:', error); } // Flush any queued messages flushMessageQueue(); }; window.ws.onmessage = (event) => { // Use requestIdleCallback or setTimeout to prevent blocking // Priority: requestIdleCallback > setTimeout(0) > setTimeout(4ms) if (window.requestIdleCallback) { window.requestIdleCallback(() => { processWebSocketMessage(event.data); }, { timeout: 1000 }); } else { setTimeout(() => { processWebSocketMessage(event.data); }, 0); } }; 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 }); window.wsReady = false; window.ws = null; // Attempt to reconnect with exponential backoff scheduleReconnect(); }; } /** * Process WebSocket message with error handling and timeout protection * @param {string} rawData - Raw message data from WebSocket */ function processWebSocketMessage(rawData) { const startTime = performance.now(); const MESSAGE_PROCESSING_TIMEOUT = 100; // 100ms max per message try { // Add timeout protection for message processing const timeoutId = setTimeout(() => { console.warn('[WebSocket] Message processing timeout, blocking detected:', { dataLength: rawData?.length || 0, elapsed: performance.now() - startTime }); }, MESSAGE_PROCESSING_TIMEOUT); const data = JSON.parse(rawData); console.log('WebSocket message received:', data.type); // Clear timeout if processing completed in time clearTimeout(timeoutId); // Use defer for heavy message handlers if (data.type === 'output' && data.data?.content?.length > 10000) { // Large message - defer processing setTimeout(() => handleWebSocketMessage(data), 0); } else { handleWebSocketMessage(data); } } catch (error) { console.error('[WebSocket] Failed to parse message:', error, 'Raw data length:', rawData?.length); } } // Exponential backoff for reconnection let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 10; const BASE_RECONNECT_DELAY = 1000; // 1 second function scheduleReconnect() { if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { console.error('[WebSocket] Max reconnection attempts reached'); if (typeof appendSystemMessage === 'function') { appendSystemMessage('⚠️ WebSocket connection lost. Please refresh the page.'); } return; } // Exponential backoff with jitter const delay = Math.min( BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttempts) + Math.random() * 1000, 30000 // Max 30 seconds ); reconnectAttempts++; console.log(`[WebSocket] Scheduling reconnect attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} in ${Math.round(delay)}ms`); setTimeout(() => { if (!window.ws || window.ws.readyState === WebSocket.CLOSED) { console.log('[WebSocket] Attempting to reconnect...'); connectWebSocket(); } }, delay); } // Reset reconnect attempts on successful connection window.ws.onopen = () => { reconnectAttempts = 0; console.log('WebSocket connected, readyState:', window.ws.readyState); window.wsReady = true; // Send a test message to verify connection try { window.ws.send(JSON.stringify({ type: 'ping' })); } catch (error) { console.error('Error sending ping:', error); } // Flush any queued messages flushMessageQueue(); }; // === WebSocket State Management === // Message queue for messages sent before WebSocket is ready window.messageQueue = []; window.wsReady = false; /** * Queue a message to be sent when WebSocket is ready * @param {Object} message - Message to queue */ function queueMessage(message) { window.messageQueue.push({ message: message, timestamp: Date.now() }); console.log(`[WebSocket] Message queued (${window.messageQueue.length} in queue):`, { type: message.type, sessionId: message.sessionId }); showQueuedMessageIndicator(); // Try to flush immediately console.log('[WebSocket] Attempting immediate flush...'); flushMessageQueue(); } /** * Flush all queued messages to WebSocket */ function flushMessageQueue() { console.log('[WebSocket] flushMessageQueue called:', { wsReady: window.wsReady, wsExists: !!window.ws, wsReadyState: window.ws?.readyState, queueLength: window.messageQueue.length }); if (!window.wsReady || !window.ws) { console.log('[WebSocket] Not ready, keeping messages in queue'); return; } if (window.messageQueue.length === 0) { console.log('[WebSocket] Queue is empty, nothing to flush'); return; } console.log(`[WebSocket] Flushing ${window.messageQueue.length} queued messages`); // Send all queued messages in batches to prevent blocking const BATCH_SIZE = 10; const batches = []; for (let i = 0; i < window.messageQueue.length; i += BATCH_SIZE) { batches.push(window.messageQueue.slice(i, i + BATCH_SIZE)); } window.messageQueue = []; // Send batches with defer to prevent blocking let batchIndex = 0; function sendNextBatch() { if (batchIndex >= batches.length) { hideQueuedMessageIndicator(); return; } const batch = batches[batchIndex]; for (const item of batch) { try { const payloadStr = JSON.stringify(item.message); console.log('[WebSocket] Sending queued message:', { type: item.message.type, sessionId: item.message.sessionId, payloadLength: payloadStr.length }); window.ws.send(payloadStr); console.log('[WebSocket] βœ“ Sent queued message:', item.message.type); } catch (error) { console.error('[WebSocket] βœ— Failed to send queued message:', error); // Put it back in the queue window.messageQueue.push(item); } } batchIndex++; // Defer next batch to prevent blocking setTimeout(sendNextBatch, 0); } sendNextBatch(); } /** * Show indicator that messages are queued */ function showQueuedMessageIndicator() { let indicator = document.getElementById('queued-message-indicator'); if (!indicator) { indicator = document.createElement('div'); indicator.id = 'queued-message-indicator'; indicator.className = 'queued-message-indicator'; indicator.innerHTML = ` ⏳ Message queued... `; // Add to chat input area const chatContainer = document.getElementById('chat-input-container'); if (chatContainer) { chatContainer.appendChild(indicator); } } indicator.style.display = 'flex'; } /** * Hide queued message indicator */ function hideQueuedMessageIndicator() { const indicator = document.getElementById('queued-message-indicator'); if (indicator && window.messageQueue.length === 0) { indicator.style.display = 'none'; } } 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 'approval-request': handleApprovalRequest(data); break; case 'approval-confirmed': handleApprovalConfirmed(data); break; case 'approval-expired': handleApprovalExpired(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); } } /** * Handle approval request event */ function handleApprovalRequest(data) { console.log('Approval request received:', data); // Only handle if we're attached to this session if (data.sessionId !== attachedSessionId) return; // Render approval card in chat if (typeof ApprovalCard !== 'undefined' && ApprovalCard.render) { const card = ApprovalCard.render(data); // Add to chat messages container const messagesContainer = document.getElementById('chat-messages'); if (messagesContainer) { messagesContainer.appendChild(card); messagesContainer.scrollTop = messagesContainer.scrollHeight; } } else { console.error('[ApprovalCard] Component not loaded'); } } /** * Handle approval confirmed event */ function handleApprovalConfirmed(data) { console.log('Approval confirmed:', data); // Only handle if we're attached to this session if (data.sessionId !== attachedSessionId) return; // Show confirmation message const message = data.approved ? (data.customCommand ? `βœ… Approved with custom command: ${data.customCommand}` : 'βœ… Command approved') : '❌ Command rejected'; if (typeof appendSystemMessage === 'function') { appendSystemMessage(message); } } /** * Handle approval expired event */ function handleApprovalExpired(data) { console.log('Approval expired:', data); // Update the approval card to show expired state if (typeof ApprovalCard !== 'undefined' && ApprovalCard.handleExpired) { ApprovalCard.handleExpired(data.id); } } /** * Detect approval request in AI response * @param {string} content - AI response content * @returns {Object|null} - Approval request data or null */ function detectApprovalRequest(content) { if (!content || typeof content !== 'string') return null; const lower = content.toLowerCase(); // Common approval request patterns const approvalPatterns = [ // "Would you like me to proceed with X" /would\s+(?:you\s+)?like\s+me\s+to\s+proceed\s+(?:with\s+)?(?:executing\s+)?(?:running\s+)?["']?([a-z0-9._\s\-\/]+)["']?/i, /should\s+i\s+(?:run|execute)\s+["']?([a-z0-9._\s\-\/]+)["']?/i, /do\s+(?:you\s+)?want\s+me\s+to\s+(?:run|execute)\s+["']?([a-z0-9._\s\-\/]+)["']?/i, /shall\s+i\s+(?:run|execute)\s+["']?([a-z0-9._\s\-\/]+)["']?/i, // "Proceed with X?" /proceed\s+(?:with\s+)?["']?([a-z0-9._\s\-\/]+)["']?\?/i, // "Execute X?" /execute\s+["']?([a-z0-9._\s\-\/]+)["']?\?/i, // "Run X?" /run\s+["']?([a-z0-9._\s\-\/]+)["']?\?/i ]; for (const pattern of approvalPatterns) { const match = content.match(pattern); if (match && match[1]) { let command = match[1].trim(); // Clean up the command command = command.replace(/[?\\s.]+$/, '').trim(); // Extract explanation from the content let explanation = ''; // Pattern 1: "this will/does X" followed by period, newline, or end const thisPattern = content.match(/this\s+(?:will|is)\s+([^.]+?)(?:\.|\n|$)/i); // Pattern 2: "operation: X" followed by period, newline, or end const operationPattern = content.match(/(?:network\s+operation|file\s+operation|system\s+operation)\s*[:\-]\s*([^.]+?)(?:\.|\n|$)/i); if (thisPattern) { explanation = thisPattern[1].trim(); } else if (operationPattern) { explanation = operationPattern[1].trim(); } // Generate a reasonable explanation if not found if (!explanation) { if (command.includes('ping')) { explanation = 'Network operation - will send ICMP packets to the target host'; } else if (command.includes('rm') || command.includes('delete')) { explanation = 'File deletion operation - files will be permanently removed'; } else if (command.includes('curl') || command.includes('wget') || command.includes('http')) { explanation = 'Network operation - will make HTTP request(s)'; } else if (command.includes('ssh') || command.includes('scp')) { explanation = 'Remote connection operation - will connect to external host'; } else { explanation = `Execute command: ${command}`; } } return { command, explanation, originalText: content }; } } return null; } /** * Escape HTML to prevent XSS * @param {string} text - Text to escape * @returns {string} - Escaped text */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } 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 on first chunk if (typeof hideStreamingIndicator === 'function') { hideStreamingIndicator(); } const content = data.data.content || ''; // ============================================================ // SEMANTIC VALIDATION - Detect confusing UX messages // ============================================================ if (window.semanticValidator && content) { const confusingOutput = window.semanticValidator.detectConfusingOutput(content); if (confusingOutput) { // Report to bug tracker window.semanticValidator.reportSemanticError(confusingOutput); // Log for debugging console.warn('[SemanticValidator] Confusing UX message detected:', confusingOutput); } } // ============================================================ // APPROVAL REQUEST DETECTION - Intercept AI approval requests // ============================================================ // Check if AI is asking for approval const approvalRequest = detectApprovalRequest(content); if (approvalRequest && window.ApprovalCard) { console.log('[ApprovalRequest] Detected:', approvalRequest); // Don't show the raw AI message - show approval card instead // But we need to still show the explanation part // Check if we already have a pending approval for this command const existingCard = document.querySelector(`[data-pending-command="${escapeHtml(approvalRequest.command)}"]`); if (!existingCard) { // Generate a unique ID for this approval const approvalId = `approval-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Render approval card const card = window.ApprovalCard.render({ id: approvalId, sessionId: data.sessionId, command: approvalRequest.command, explanation: approvalRequest.explanation || 'AI agent requests approval to execute this command' }); // Add tracking attribute card.dataset.pendingCommand = approvalRequest.command; // Add to chat const messagesContainer = document.getElementById('chat-messages'); if (messagesContainer) { messagesContainer.appendChild(card); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Store the approval for later response window._pendingApprovals = window._pendingApprovals || {}; window._pendingApprovals[approvalId] = { command: approvalRequest.command, sessionId: data.sessionId, originalMessage: content }; // Don't add the AI's raw text to the chat // Just skip to next iteration return; } } // Accumulate streaming content if (streamingMessageElement && streamingMessageElement.isConnected) { // Append to existing message streamingMessageContent += content; const bubble = streamingMessageElement.querySelector('.chat-message-bubble'); if (bubble && typeof formatMessage === 'function') { bubble.innerHTML = formatMessage(streamingMessageContent); } } else { // Start new streaming message streamingMessageContent = content; if (typeof appendMessage === 'function') { appendMessage('assistant', content, true); // Get the message element we just created streamingMessageElement = document.querySelector('.chat-message.assistant:last-child'); } } // Reset streaming timeout - if no new chunks for 1 second, consider stream complete clearTimeout(streamingTimeout); streamingTimeout = setTimeout(() => { streamingMessageElement = null; streamingMessageContent = ''; if (typeof setGeneratingState === 'function') { setGeneratingState(false); } // ============================================================ // COMMAND TRACKER - Complete command execution // ============================================================ if (window.commandTracker && window._pendingCommandId) { // Try to extract exit code from the last output const exitCode = extractExitCode(streamingMessageContent); // Complete the command tracking window.commandTracker.completeCommand( window._pendingCommandId, exitCode, streamingMessageContent ); // Clear pending command ID window._pendingCommandId = null; console.log('[CommandTracker] Command completed via stream timeout'); } }, 1000); } /** * Extract exit code from command output * @param {string} output - Command output * @returns {number|null} - Exit code or null */ function extractExitCode(output) { if (!output) return null; // Look for exit code patterns const exitCodeMatch = output.match(/exited with code (\d+|undefined|null)/i); if (exitCodeMatch) { const code = exitCodeMatch[1]; if (code === 'undefined' || code === 'null') { return null; // Undefined/null exit code } return parseInt(code, 10); } // If no explicit exit code found, assume success (0) return 0; } } // Dashboard async function loadDashboard() { try { // Load stats with timeout protection const [sessionsRes, projectsRes] = await Promise.all([ fetchWithTimeout('/claude/api/claude/sessions', 5000), fetchWithTimeout('/claude/api/claude/projects', 5000) ]); 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 - CLEAR LOADING STATE const activeSessionsEl = document.getElementById('active-sessions-list'); if (sessionsData.active && sessionsData.active.length > 0) { activeSessionsEl.innerHTML = sessionsData.active.map(session => `
${session.id.substring(0, 20)}... ${session.status}
Working: ${session.workingDir}
Created: ${new Date(session.createdAt).toLocaleString()}
`).join(''); } else { // Clear loading state, show empty state activeSessionsEl.innerHTML = '

No active sessions

'; } // Update projects list - CLEAR LOADING STATE const projectsEl = document.getElementById('recent-projects-list'); if (projectsData.projects && projectsData.projects.length > 0) { projectsEl.innerHTML = projectsData.projects.slice(0, 5).map(project => `

${project.name}

Modified: ${new Date(project.modified).toLocaleDateString()}

`).join(''); } else { // Clear loading state, show empty state projectsEl.innerHTML = '

No projects yet

'; } } catch (error) { console.error('Error loading dashboard:', error); // Clear loading states on error const activeSessionsEl = document.getElementById('active-sessions-list'); const projectsEl = document.getElementById('recent-projects-list'); if (activeSessionsEl) activeSessionsEl.innerHTML = '

Error loading sessions

'; if (projectsEl) projectsEl.innerHTML = '

Error loading projects

'; } } 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 = '
Loading sessions...
'; const res = await fetchWithTimeout(apiUrl, 5000); // Handle HTTP errors if (!res.ok) { if (res.status === 401) { sessionsListEl.innerHTML = `

⚠️ Session expired

`; 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 - CLEAR LOADING STATE if (allSessions.length === 0) { const projectName = projectPath ? projectPath.split('/').pop() : 'this project'; sessionsListEl.innerHTML = `
πŸ“‚

No sessions found for ${escapeHtml(projectName)}

`; return; } // Render session list - CLEAR LOADING STATE 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 `
${session.id.substring(0, 12)}... ${isRunning ? '🟒 Running' : '⏸️ ' + (session.type === 'historical' ? 'Historical' : 'Stopped')}
${relativeTime}
πŸ“ ${escapeHtml(session.workingDir)}
πŸ’¬ ${messageCount} messages
`; }).join(''); // CRITICAL FIX: Also update session tabs with the same sessions if (window.sessionTabs && typeof window.sessionTabs.setSessions === 'function') { window.sessionTabs.setSessions(allSessions); console.log('[loadSessions] Updated session tabs with', allSessions.length, 'sessions'); } } catch (error) { console.error('[loadSessions] Error:', error); sessionsListEl.innerHTML = `
⚠️

Failed to load sessions

${escapeHtml(error.message)}

`; } } 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; } // Delete Session async function deleteSession(sessionId, event) { // Prevent triggering the session item click if (event) { event.stopPropagation(); event.preventDefault(); } // Confirm deletion const shortId = sessionId.substring(0, 12); if (!confirm(`Delete session ${shortId}...?\n\nThis action cannot be undone.`)) { return; } try { console.log('[deleteSession] Deleting session:', sessionId); const res = await fetch(`/claude/api/claude/sessions/${sessionId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, credentials: 'include' }); if (!res.ok) { throw new Error(`HTTP ${res.status}: ${res.statusText}`); } const data = await res.json(); if (data.success || data.deleted) { console.log('[deleteSession] Session deleted successfully'); // If the deleted session was the current one, clear current session state if (attachedSessionId === sessionId || chatSessionId === sessionId) { console.log('[deleteSession] Deleted current session, clearing state'); attachedSessionId = null; chatSessionId = null; // Clear UI document.getElementById('current-session-id').textContent = 'None'; document.getElementById('chat-title').textContent = 'Claude Code IDE'; clearChatDisplay(); } // Refresh the session list await loadSessions(); // Show success message if (typeof appendSystemMessage === 'function') { appendSystemMessage(`βœ… Session ${shortId}... deleted`); } } else { throw new Error(data.error || 'Failed to delete session'); } } catch (error) { console.error('[deleteSession] Error:', error); if (typeof appendSystemMessage === 'function') { appendSystemMessage(`❌ Failed to delete session: ${error.message}`); } else { alert(`Failed to delete session: ${error.message}`); } } } async function viewSessionDetails(sessionId) { const detailEl = document.getElementById('session-detail'); try { // Show loading state detailEl.innerHTML = '
Loading session details...
'; const res = await fetchWithTimeout(`/claude/api/claude/sessions/${sessionId}`, 5000); // Handle 404 - session not found if (res.status === 404) { detailEl.innerHTML = `
πŸ”

Session Not Found

The session ${escapeHtml(sessionId)} could not be found.

`; 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 - CLEAR LOADING STATE detailEl.innerHTML = `

Session ${session.id.substring(0, 12)}...

${isRunning ? '🟒 Running' : '⏸️ Stopped'}
${isRunning ? ` ` : ''}
Working Directory: ${escapeHtml(session.workingDir)}
Created: ${new Date(session.createdAt).toLocaleString()}
Last Activity: ${new Date(session.lastActivity).toLocaleString()}
Messages: ${messageCount}
${session.pid ? `
PID: ${session.pid}
` : ''}

Token Usage

${(session.context?.totalTokens || 0).toLocaleString()} / ${(session.context?.maxTokens || 200000).toLocaleString()} tokens ${Math.round((session.context?.totalTokens || 0) / (session.context?.maxTokens || 200000) * 100)}% used

Session Output (${messageCount} entries)

${session.outputBuffer?.slice(0, 50).map(entry => `
${entry.type} ${new Date(entry.timestamp).toLocaleTimeString()}
${escapeHtml(entry.content.substring(0, 500))}${entry.content.length > 500 ? '...' : ''}
`).join('') || '

No output yet

'} ${session.outputBuffer?.length > 50 ? `

...and ${session.outputBuffer.length - 50} more entries

` : ''}
`; currentSession = session; } catch (error) { console.error('[viewSessionDetails] Error:', error); detailEl.innerHTML = `
⚠️

Failed to Load Session

${escapeHtml(error.message)}

`; } } async function continueSessionInChat(sessionId) { console.log('[Sessions] Continuing session in Chat:', sessionId); try { showLoadingOverlay('Loading session...'); const res = await fetchWithTimeout(`/claude/api/claude/sessions/${sessionId}`, 5000); 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; hideLoadingOverlay(); 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 fetchWithTimeout(`/claude/api/claude/sessions/${sessionId}`, 5000); 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 fetchWithTimeout('/claude/api/claude/sessions', 5000, { 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 fetchWithTimeout(`/claude/api/claude/sessions/${sessionId}`, 5000, { 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 = `

Session Terminated

Select another session from the sidebar

`; 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 fetchWithTimeout('/claude/api/claude/projects', 5000); const data = await res.json(); const gridEl = document.getElementById('projects-grid'); if (data.projects && data.projects.length > 0) { gridEl.innerHTML = data.projects.map(project => `

${project.name}

Modified: ${new Date(project.modified).toLocaleString()}

Click to view project details

`).join(''); } else { gridEl.innerHTML = '

No projects yet. Create your first project!

'; } } 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 fetchWithTimeout('/claude/api/files', 5000); 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 `
${icon} ${item.name}
`; } else { return `
${icon} ${item.name}
`; } }).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 fetchWithTimeout(`/claude/api/file/${encodeURIComponent(filePath)}`, 5000); const data = await res.json(); // Check if Monaco Editor component is available if (window.monacoEditor) { // Use the Monaco-based editor await window.monacoEditor.openFile(filePath, data.content || ''); } else { // Fallback to simple view console.warn('[loadFile] Monaco Editor not available, using fallback'); const editorEl = document.getElementById('file-editor'); const isHtmlFile = filePath.toLowerCase().endsWith('.html') || filePath.toLowerCase().endsWith('.htm'); if (isHtmlFile) { // HTML file - show with preview option editorEl.innerHTML = `

${filePath}

${escapeHtml(data.content || '')}
`; // 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 content const language = getLanguageFromFile(filePath); editorEl.innerHTML = `

${filePath}

${language}
${escapeHtml(data.content || '')}
`; } } } catch (error) { console.error('Error loading file:', error); const editorEl = document.getElementById('file-editor'); if (editorEl) { editorEl.innerHTML = `

Error loading file

${error.message}

`; } } } // Helper function to get language from file path function getLanguageFromFile(filePath) { const ext = filePath.split('.').pop().toLowerCase(); const languageMap = { 'js': 'JavaScript', 'jsx': 'JavaScript JSX', 'ts': 'TypeScript', 'tsx': 'TypeScript JSX', 'py': 'Python', 'html': 'HTML', 'css': 'CSS', 'json': 'JSON', 'md': 'Markdown', 'txt': 'Plain Text' }; return languageMap[ext] || 'Plain Text'; } 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 fetchWithTimeout('/claude/api/claude/sessions', 5000, { 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 fetchWithTimeout('/claude/api/claude/projects', 5000, { 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 = `

${escapeHtml(message)}

`; 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 = ` ${getToastIcon(type)} ${escapeHtml(message)} `; 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; } /** * Fetch with timeout protection to prevent hanging requests * @param {string} url - The URL to fetch * @param {number} timeout - Timeout in milliseconds * @param {object} options - Fetch options * @returns {Promise} */ async function fetchWithTimeout(url, timeout = 5000, options = {}) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error(`Request timeout after ${timeout}ms`); } throw error; } } 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); } });