// 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(); // ============================================================ // SSE APPROACH: DO NOT create WebSocket connection // SSE client (sse-client.js) handles all server events now // ============================================================ // connectWebSocket(); // DISABLED - Using SSE instead // ============================================================ // HYBRID APPROACH: Parse session ID from URL path (route-based) // ============================================================ // New URL format: /claude/ide/session/{sessionId} // Old URL format: /claude/ide?session={sessionId} (with redirect) let sessionId = null; let prompt = null; let project = null; let view = null; // Try to extract sessionId from URL path first (new format) const pathMatch = window.location.pathname.match(/\/claude\/ide\/session\/([^/]+)$/); if (pathMatch && pathMatch[1]) { sessionId = decodeURIComponent(pathMatch[1]); console.log('[Init] Session ID from URL path:', sessionId); } // Fall back to query parameters (for backward compatibility during migration) if (!sessionId) { const urlParams = new URLSearchParams(window.location.search); sessionId = urlParams.get('session'); prompt = urlParams.get('prompt'); project = urlParams.get('project'); view = urlParams.get('view'); if (sessionId) { console.log('[Init] Session ID from query params (old format - should redirect):', sessionId); } } else { // Parse other parameters from query string const urlParams = new URLSearchParams(window.location.search); prompt = urlParams.get('prompt'); project = urlParams.get('project'); 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); } // ============================================================ // HYBRID APPROACH: Session context from URL path (immediate) // ============================================================ // With route-based URLs (/claude/ide/session/{id}), the session // context is available immediately from the URL path - no // attachment race condition possible! if (sessionId || prompt) { // Switch to chat view switchView('chat'); // Set session context immediately (no async attachment needed) if (sessionId) { // Route-based URLs mean session context is immediate // The attachedSessionId is set before any user interaction can occur window.attachedSessionId = sessionId; window.chatSessionId = sessionId; console.log('[Init] Session context from URL path (no attachment needed):', sessionId); // Register SSE event handlers for this session if (typeof registerSSEEventHandlers === 'function') { registerSSEEventHandlers(sessionId); console.log('[Init] SSE event handlers registered for session:', sessionId); } } // Handle prompt parameter if (prompt) { requestAnimationFrame(() => { const input = document.getElementById('chat-input'); if (input) { input.value = decodeURIComponent(prompt); sendChatMessage(); } }); } } 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); // 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) => { 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 }); window.wsReady = false; window.ws = null; // Attempt to reconnect after 5 seconds setTimeout(() => { console.log('Attempting to reconnect...'); connectWebSocket(); }, 5000); }; } // === 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 const messagesToSend = [...window.messageQueue]; window.messageQueue = []; for (const item of messagesToSend) { 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); } } hideQueuedMessageIndicator(); } /** * 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(); // First, check if this looks like an approval response at all const approvalIndicators = [ /requires?\s+approval/i, /need\s+approval/i, /waiting\s+for\s+approval/i, /awaiting\s+approval/i, /approve?\s+(?:before\s+)?(?:i\s+)?(?:can\s+)?(?:proceed|continue|execute)/i ]; const hasApprovalIndicator = approvalIndicators.some(pattern => pattern.test(content)); if (!hasApprovalIndicator) { return null; } // 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 ]; // Try to extract command from the patterns 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 = ''; const explanationMatch = content.match(/(?:this\s+(?:will|is)\s+.+?|(?:network|file|system)\s+operation\s*[:-]\s*.+?)(?:\.|\n|$)/i); if (explanationMatch) { explanation = (explanationMatch[1] || explanationMatch[2] || '').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 }; } } // Fallback: Try to extract command from "The X command requires approval" pattern const fallbackCommandMatch = content.match(/(?:the\s+)?["']?([a-z0-9._\s\-\/]+)["']?\s+command\s+requires?\s+approval/i); if (fallbackCommandMatch && fallbackCommandMatch[1]) { let command = fallbackCommandMatch[1].trim(); let explanation = 'This command requires approval before execution'; return { command, explanation }; } 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; } // Streaming message state for accumulating response chunks let streamingMessageElement = null; let streamingMessageContent = ''; let streamingTimeout = null; 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 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 => `
No active sessions
'; } // 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 => `No projects yet
'; } } 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 = '⚠️ Session expired
No sessions found for ${escapeHtml(projectName)}
Failed to load sessions
The session ${escapeHtml(sessionId)} could not be found.
No output yet
'} ${session.outputBuffer?.length > 50 ? `...and ${session.outputBuffer.length - 50} more entries
` : ''}Select another session from the sidebar
Click to view project details
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 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 `${escapeHtml(data.content || '')}
${escapeHtml(data.content || '')}
${error.message}
${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 = ` `; 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); } }); console.log('=== NEW VERSION LOADED: 1769011300000 ===');