// ============================================ // Chat Interface Functions // ============================================ let chatSessionId = null; let chatMessages = []; let attachedSessionId = null; let isGenerating = false; // Track if Claude is currently generating let modeSuggestionTimeout = null; // Track mode suggestion auto-hide timer // Reset all chat state function resetChatState() { console.log('Resetting chat state...'); chatSessionId = null; chatMessages = []; attachedSessionId = null; console.log('Chat state reset complete'); } // Load Chat View async function loadChatView() { console.log('[loadChatView] Loading chat view...'); if (window.traceExecution) { window.traceExecution('chat-functions', 'loadChatView called', { pendingSessionId: window.pendingSessionId, pendingSessionAttach: window.pendingSessionAttach, PRELOAD_SESSION_ID: window.PRELOAD_SESSION_ID }); } // Check if there's a pending session from Sessions view if (window.pendingSessionId) { console.log('[loadChatView] Detected pending session:', window.pendingSessionId); if (window.traceExecution) { window.traceExecution('chat-functions', 'Detected pendingSessionId', { sessionId: window.pendingSessionId }); } const sessionId = window.pendingSessionId; const sessionData = window.pendingSessionData; // Clear pending session (consume it) window.pendingSessionId = null; window.pendingSessionData = null; // Load the session await loadSessionIntoChat(sessionId, sessionData); return; } // Preserve attached session ID if it exists (for auto-session workflow) const preservedSessionId = attachedSessionId; // Reset state on view load to prevent stale session references resetChatState(); // Restore attached session if it was set (e.g., from auto-session initialization) if (preservedSessionId) { console.log('[loadChatView] Restoring attached session:', preservedSessionId); attachedSessionId = preservedSessionId; chatSessionId = preservedSessionId; } // Load chat sessions try { console.log('[loadChatView] Fetching sessions...'); const res = await fetch('/claude/api/claude/sessions'); if (!res.ok) { throw new Error(`HTTP ${res.status}: ${await res.text()}`); } const data = await res.json(); console.log('[loadChatView] Raw sessions data:', { activeCount: (data.active || []).length, historicalCount: (data.historical || []).length, activeIds: (data.active || []).map(s => ({ id: s.id, status: s.status })) }); const sessionsListEl = document.getElementById('chat-history-list'); if (!sessionsListEl) { console.error('[loadChatView] chat-history-list element not found!'); return; } // ONLY show active sessions - no historical sessions in chat view // Historical sessions are read-only and can't receive new messages // FIX: Show all sessions in the active array, not just those with status='running' // The active array contains sessions that are in memory and can receive messages let activeSessions = (data.active || []); console.log('[loadChatView] Running sessions after status filter:', activeSessions.length); // Filter by current project if in project context const currentProjectDir = window.currentProjectDir; if (currentProjectDir) { console.log('[loadChatView] Current project dir:', currentProjectDir); // Filter sessions that belong to this project activeSessions = activeSessions.filter(session => { // Check if session's working directory is within current project directory const sessionWorkingDir = session.workingDir || ''; // Direct match: session working dir starts with project dir const directMatch = sessionWorkingDir.startsWith(currentProjectDir); // Metadata match: session metadata project matches const metadataMatch = session.metadata?.project === currentProjectDir; // For project sessions, also check if project path is in working dir const pathMatch = sessionWorkingDir.includes(currentProjectDir) || currentProjectDir.includes(sessionWorkingDir); const isMatch = directMatch || metadataMatch || pathMatch; console.log(`[loadChatView] Session ${session.id}:`, { workingDir: sessionWorkingDir, projectDir: currentProjectDir, directMatch, metadataMatch, pathMatch, isMatch }); return isMatch; }); console.log('[loadChatView] Project sessions found:', activeSessions.length, 'out of', (data.active || []).length); } console.log('Active sessions (can receive messages):', activeSessions.length); // ============================================================ // URL-BASED SESSION ATTACHMENT (Always check first!) // ============================================================ // If the URL contains a session ID (/claude/ide/session/XXX), // ALWAYS attempt to attach to that session first, regardless of // whether there are other active sessions. This handles the case // where a user navigates directly to a specific session URL. // FIRST PRIORITY: Check PRELOAD_SESSION_ID (set by inline script, guaranteed to exist) let pendingSessionId = window.PRELOAD_SESSION_ID; // SECOND: Check the flag set by ide.js if (!pendingSessionId) { pendingSessionId = window.pendingSessionAttach; } // THIRD: Check the URL pathname directly (fallback) if (!pendingSessionId) { const pathname = window.location.pathname; const sessionMatch = pathname.match(/\/claude\/ide\/session\/([^\/]+)$/); if (sessionMatch && sessionMatch[1]) { pendingSessionId = sessionMatch[1]; console.log('[loadChatView] Found sessionId in URL pathname:', pendingSessionId); } } // FOURTH: Check legacy query parameter if (!pendingSessionId) { const urlParams = new URLSearchParams(window.location.search); pendingSessionId = urlParams.get('session'); } const hasPendingAttach = !!pendingSessionId; if (hasPendingAttach) { console.log('[loadChatView] Pending session attachment detected:', pendingSessionId); console.log('[loadChatView] Attaching IMMEDIATELY (no delay)'); if (window.AutoFixLogger) { window.AutoFixLogger.success('Session attachment in progress', { sessionId: pendingSessionId }); } if (window.traceExecution) { window.traceExecution('chat-functions', 'Pending session attachment detected - attaching IMMEDIATELY', { sessionId: pendingSessionId }); } sessionsListEl.innerHTML = `

Loading session: ${pendingSessionId.substring(0, 20)}...

`; // Attach IMMEDIATELY - don't wait for setTimeout! // Use setTimeout(..., 0) to allow UI to update first setTimeout(() => { attachToSession(pendingSessionId); if (window.AutoFixLogger) { window.AutoFixLogger.success('Session attached successfully', { sessionId: pendingSessionId }); } if (window.traceExecution) { window.traceExecution('chat-functions', 'Called attachToSession successfully', { sessionId: pendingSessionId }); } }, 0); return; } // ============================================================ // No URL-based session attachment - render session list normally // ============================================================ if (activeSessions.length > 0) { sessionsListEl.innerHTML = activeSessions.map(session => { const projectName = session.metadata && session.metadata.project ? session.metadata.project : session.id.substring(0, 20); return `
πŸ’¬
${projectName}
${new Date(session.createdAt).toLocaleDateString()} Running
`; }).join(''); } else { // No active sessions and no URL-based session to attach to if (currentProjectName && window.currentProjectDir) { // Zero-friction entry: Auto-create session in project context console.log('[loadChatView] No sessions for project, auto-creating...'); sessionsListEl.innerHTML = `

Creating session for project: ${currentProjectName}

`; // Auto-create session startNewChat(); } else { const emptyMessage = `

No active sessions

`; sessionsListEl.innerHTML = `
${emptyMessage}
`; } } console.log('[loadChatView] Chat view loaded successfully'); } catch (error) { console.error('[loadChatView] Error loading chat sessions:', error); const sessionsListEl = document.getElementById('chat-history-list'); if (sessionsListEl) { sessionsListEl.innerHTML = `

Error: ${error.message}

`; } } } /** * Load a specific session into Chat view * Called when continuing from Sessions view * Uses batching and async/defer to prevent UI blocking */ async function loadSessionIntoChat(sessionId, sessionData = null) { try { appendSystemMessage('πŸ“‚ Loading session...'); // If no session data provided, fetch it if (!sessionData) { const res = await fetch(`/claude/api/claude/sessions/${sessionId}`); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } const data = await res.json(); sessionData = data.session; } if (!sessionData) { throw new Error('Session not found'); } // Set session IDs attachedSessionId = sessionId; chatSessionId = sessionId; // Update UI document.getElementById('current-session-id').textContent = sessionId; // Clear chat display clearChatDisplay(); // Load session messages (both user and assistant) // IMPORTANT: Process messages in batches to prevent blocking if (sessionData.outputBuffer && sessionData.outputBuffer.length > 0) { const messages = sessionData.outputBuffer; const BATCH_SIZE = 20; const totalMessages = messages.length; console.log(`[loadSessionIntoChat] Loading ${totalMessages} messages in batches of ${BATCH_SIZE}`); // Process first batch immediately const firstBatch = messages.slice(0, BATCH_SIZE); for (const entry of firstBatch) { if (entry.role) { appendMessage(entry.role, entry.content, false); } else { // Legacy format - default to assistant appendMessage('assistant', entry.content, false); } } // Process remaining batches in deferred chunks if (totalMessages > BATCH_SIZE) { let currentIndex = BATCH_SIZE; function processNextBatch() { const batch = messages.slice(currentIndex, currentIndex + BATCH_SIZE); for (const entry of batch) { if (entry.role) { appendMessage(entry.role, entry.content, false); } else { // Legacy format - default to assistant appendMessage('assistant', entry.content, false); } } currentIndex += BATCH_SIZE; // Show progress if there are many messages if (totalMessages > 100 && currentIndex % (BATCH_SIZE * 5) === 0) { const progress = Math.round((currentIndex / totalMessages) * 100); appendSystemMessage(`⏳ Loading messages... ${progress}%`); } // Continue with next batch using requestIdleCallback or setTimeout if (currentIndex < totalMessages) { if (window.requestIdleCallback) { window.requestIdleCallback(processNextBatch, { timeout: 50 }); } else { setTimeout(processNextBatch, 0); } } else { // All messages loaded onSessionMessagesLoaded(sessionData); } } // Start processing batches if (window.requestIdleCallback) { window.requestIdleCallback(processNextBatch, { timeout: 50 }); } else { setTimeout(processNextBatch, 0); } } else { // All messages loaded in first batch onSessionMessagesLoaded(sessionData); } } else { // No messages to load onSessionMessagesLoaded(sessionData); } } catch (error) { console.error('[loadSessionIntoChat] Error:', error); appendSystemMessage('❌ Failed to load session: ' + error.message); } } /** * Called when all session messages have been loaded */ function onSessionMessagesLoaded(sessionData) { const isRunning = sessionData.status === 'running'; const statusText = isRunning ? 'Active session' : 'Historical session'; appendSystemMessage(`βœ… Loaded ${statusText} from ${new Date(sessionData.createdAt).toLocaleString()}`); if (!isRunning) { appendSystemMessage('ℹ️ This is a historical session. Messages are read-only.'); } // Update chat history sidebar to highlight this session if (typeof loadChatHistory === 'function') { loadChatHistory(); } // Subscribe to session for live updates (if running) if (isRunning) { subscribeToSession(sessionData.id); } // Focus input for active sessions if (isRunning) { setTimeout(() => { const input = document.getElementById('chat-input'); if (input) input.focus(); }, 100); } } // Start New Chat async function startNewChat() { // Reset all state first resetChatState(); // Clear current chat clearChatDisplay(); appendSystemMessage('Creating new chat session...'); // Determine working directory based on context let workingDir = '/home/uroma/obsidian-vault'; // default let projectName = null; // If we're in a project context, use the project directory if (window.currentProjectDir) { workingDir = window.currentProjectDir; projectName = window.currentProjectDir.split('/').pop(); console.log('[startNewChat] Creating session for project:', projectName, 'at', workingDir); } // Create new session try { console.log('Creating new Claude Code session...'); const res = await fetch('/claude/api/claude/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ workingDir: workingDir, metadata: { type: 'chat', source: 'web-ide', project: projectName, projectPath: window.currentProjectDir || null } }) }); const data = await res.json(); console.log('Session creation response:', data); if (data.success) { attachedSessionId = data.session.id; chatSessionId = data.session.id; console.log('New session created:', data.session.id); // Update UI document.getElementById('current-session-id').textContent = data.session.id; document.getElementById('chat-title').textContent = projectName ? `Project: ${projectName}` : 'New Chat'; // Subscribe to session via WebSocket subscribeToSession(data.session.id); // Give backend time to persist session, then refresh sidebar // This ensures the new session appears in the list await new Promise(resolve => setTimeout(resolve, 150)); // Use loadChatHistory instead of loadChatView to avoid triggering URL-based attachment if (typeof loadChatHistory === 'function') { await loadChatHistory().catch(err => console.error('[startNewChat] Background refresh failed:', err)); } else if (typeof window.refreshSessionList === 'function') { await window.refreshSessionList().catch(err => console.error('[startNewChat] Background refresh failed:', err)); } // Hide the creation success message after a short delay setTimeout(() => { const loadingMsg = document.querySelector('.chat-system'); if (loadingMsg && loadingMsg.textContent.includes('Creating new chat session')) { loadingMsg.remove(); } }, 2000); // Focus on input const input = document.getElementById('chat-input'); if (input) { input.focus(); } } else { throw new Error(data.error || 'Failed to create session'); } } catch (error) { console.error('Error creating new chat session:', error); appendSystemMessage('❌ Failed to create new chat session: ' + error.message); } } // Attach to Existing Session function attachToSession(sessionId) { // ============================================================ // RACE CONDITION FIX: Debug logging // ============================================================ console.log('[attachToSession] ===== STARTING SESSION ATTACHMENT ======'); console.log('[attachToSession] sessionId parameter:', sessionId); console.log('[attachToSession] Current attachedSessionId BEFORE:', attachedSessionId); console.log('[attachToSession] Current chatSessionId BEFORE:', chatSessionId); console.log('[attachToSession] window.pendingSessionAttach:', window.pendingSessionAttach); if (window.traceExecution) { window.traceExecution('chat-functions', 'attachToSession START', { sessionId, attachedSessionId_BEFORE: attachedSessionId, chatSessionId_BEFORE: chatSessionId, pendingSessionAttach: window.pendingSessionAttach }); } // Clear the intent flag now that we're attaching if (window.pendingSessionAttach === sessionId) { window.pendingSessionAttach = null; console.log('[attachToSession] βœ… Cleared pending session attachment flag'); } attachedSessionId = sessionId; chatSessionId = sessionId; console.log('[attachToSession] βœ… Set attachedSessionId to:', attachedSessionId); console.log('[attachToSession] βœ… Set chatSessionId to:', chatSessionId); console.log('[attachToSession] ===== SESSION ATTACHMENT COMPLETE ======'); if (window.traceExecution) { window.traceExecution('chat-functions', 'attachToSession COMPLETE', { sessionId, attachedSessionId_AFTER: attachedSessionId, chatSessionId_AFTER: chatSessionId }); } // Update UI document.getElementById('current-session-id').textContent = sessionId; // Update context panel with session if (typeof contextPanel !== 'undefined' && contextPanel) { contextPanel.setSession(sessionId, 'active'); } // Load session messages loadSessionMessages(sessionId); // Safety timeout: Clear loading state if stuck after 3 seconds setTimeout(() => { const sessionsListEl = document.getElementById('chat-history-list'); if (sessionsListEl && sessionsListEl.textContent.includes('Loading session')) { console.warn('[attachToSession] Loading stuck - forcing clear'); if (window.traceExecution) { window.traceExecution('chat-functions', 'Loading stuck - forcing clear', { sessionId }); } sessionsListEl.innerHTML = `

βœ… Session ready

Send a message to start chatting

`; } }, 3000); // ============================================================ // HYBRID APPROACH: Connect SSE instead of WebSocket subscription // ============================================================ // With SSE, we connect to the session's event stream directly // No need to "subscribe" - the connection is session-scoped by URL if (window.sseClient && window.sseClient.currentSessionId !== sessionId) { console.log('[attachToSession] Connecting SSE to session:', sessionId); window.sseClient.connect(sessionId); // Register SSE event handlers for this session registerSSEEventHandlers(sessionId); } // Update active state in sidebar document.querySelectorAll('.chat-session-item').forEach(item => { item.classList.remove('active'); if (item.getAttribute('onclick') && item.getAttribute('onclick').includes(sessionId)) { item.classList.add('active'); } }); appendSystemMessage('Attached to session: ' + sessionId); // Refresh the session list in sidebar to show this session // Use a flag to prevent infinite recursion since loadChatView might call attachToSession if (typeof window.refreshSessionList === 'function') { window.refreshSessionList(); } } // Refresh session list without triggering attachment window.refreshSessionList = async function() { try { const res = await fetch('/claude/api/claude/sessions'); if (!res.ok) return; const data = await res.json(); const activeSessions = data.active || []; const sessionsListEl = document.getElementById('chat-history-list'); if (!sessionsListEl) return; // Only update if we're not showing "Loading session..." if (sessionsListEl.textContent.includes('Loading session')) { return; } if (activeSessions.length === 0) { sessionsListEl.innerHTML = `

βœ… Session ready

Send a message to start chatting

`; return; } sessionsListEl.innerHTML = activeSessions.map(session => { const projectName = session.metadata && session.metadata.project ? session.metadata.project : session.id.substring(0, 20); const isActive = session.id === attachedSessionId ? 'active' : ''; return `
πŸ’¬
${projectName}
${new Date(session.createdAt).toLocaleDateString()} Running
`; }).join(''); } catch (error) { console.error('[refreshSessionList] Error:', error); } }; // ============================================================ // HYBRID APPROACH: Register SSE event handlers // ============================================================ // Map SSE events to the existing WebSocket message handlers let SSE_HANDLERS_REGISTERED = false; // Guard flag to prevent duplicate registrations function registerSSEEventHandlers(sessionId) { if (!window.sseClient) return; // GUARD: Only register handlers once to prevent duplicate AI responses if (SSE_HANDLERS_REGISTERED) { console.log('[registerSSEEventHandlers] Handlers already registered, skipping duplicate registration'); return; } SSE_HANDLERS_REGISTERED = true; console.log('[registerSSEEventHandlers] Registering handlers for first time'); // Session output - handle AI responses window.sseClient.on('session-output', (event) => { console.log('[SSE] session-output:', event); if (window.traceExecution) { window.traceExecution('chat-functions', 'SSE session-output received', { sessionId: event.sessionId, type: event.type, contentLength: event.content?.length || 0, contentPreview: event.content?.substring(0, 100) || '', attachedSessionId }); } // Pass event directly - handleSessionOutput expects data.data.content structure handleSessionOutput({ sessionId: event.sessionId, data: { type: event.type || 'stdout', content: event.content, timestamp: event.timestamp || Date.now() } }); }); // Session error - handle errors window.sseClient.on('session-error', (event) => { console.log('[SSE] session-error:', event); if (window.traceExecution) { window.traceExecution('chat-functions', 'SSE session-error received', { sessionId: event.sessionId, error: event.error, code: event.code }); } // Auto-report error for fixing if (window.AutoFixLogger) { window.AutoFixLogger.log('SSE session error', { sessionId: event.sessionId, error: event.error, code: event.code }); } // Pass event directly handleSessionOutput({ sessionId: event.sessionId, data: { type: 'error', error: event.error, code: event.code, timestamp: event.timestamp || Date.now() } }); }); // Approval request - handle terminal command approvals window.sseClient.on('approval-request', (event) => { console.log('[SSE] approval-request:', event); // Trigger approval UI if (typeof handleApprovalRequest === 'function') { handleApprovalRequest(event); } }); // Approval confirmed/expired window.sseClient.on('approval-confirmed', (event) => { console.log('[SSE] approval-confirmed:', event); }); window.sseClient.on('approval-expired', (event) => { console.log('[SSE] approval-expired:', event); }); console.log('[SSE] Event handlers registered for session:', sessionId); } // Subscribe to session via WebSocket (LEGACY - for backward compatibility) // This function is deprecated and will be removed once SSE is fully integrated function subscribeToSession(sessionId) { // ============================================================ // HYBRID APPROACH: SSE replaces WebSocket subscription // ============================================================ // SSE connections are session-scoped by URL, so no explicit // subscription is needed. The SSE client handles this automatically. if (window.sseClient && window.sseClient.currentSessionId !== sessionId) { console.log('[subscribeToSession] Connecting SSE (replaces WebSocket subscription):', sessionId); window.sseClient.connect(sessionId); registerSSEEventHandlers(sessionId); return; } // Fallback to WebSocket if SSE not available if (window.ws && window.ws.readyState === WebSocket.OPEN) { window.ws.send(JSON.stringify({ type: 'subscribe', sessionId: sessionId })); console.log('[LEGACY] Subscribed to session via WebSocket:', sessionId); } else if (window.ws && window.ws.readyState === WebSocket.CONNECTING) { // Wait for connection to open, then subscribe console.log('[subscribeToSession] WebSocket connecting, will subscribe when ready...'); const onOpen = () => { window.ws.send(JSON.stringify({ type: 'subscribe', sessionId: sessionId })); console.log('[subscribeToSession] Subscribed after connection open:', sessionId); window.ws.removeEventListener('open', onOpen); }; window.ws.addEventListener('open', onOpen); } else { // WebSocket not connected - try to reconnect console.warn('[subscribeToSession] WebSocket not connected, attempting to connect...'); if (typeof connectWebSocket === 'function') { connectWebSocket(); // Retry subscription after connection setTimeout(() => subscribeToSession(sessionId), 500); } } } // Load Session Messages // Uses batching to prevent UI blocking with large message buffers async function loadSessionMessages(sessionId) { if (window.traceExecution) { window.traceExecution('chat-functions', 'loadSessionMessages START', { sessionId }); } try { const res = await fetch('/claude/api/claude/sessions/' + sessionId); if (window.traceExecution) { window.traceExecution('chat-functions', 'loadSessionMessages fetch response', { sessionId, status: res.status, ok: res.ok }); } if (!res.ok) { if (window.traceExecution) { window.traceExecution('chat-functions', 'loadSessionMessages fetch FAILED', { sessionId, status: res.status }); } throw new Error(`HTTP ${res.status}`); } const data = await res.json(); if (data.session) { clearChatDisplay(); const messages = data.session.outputBuffer; const BATCH_SIZE = 20; const totalMessages = messages.length; if (totalMessages === 0) { // New session with no messages yet - clear loading state and show ready state if (window.traceExecution) { window.traceExecution('chat-functions', 'loadSessionMessages - empty session, showing chat interface', { sessionId }); } // Clear the "Loading..." message from the sidebar and refresh to show all sessions const sessionsListEl = document.getElementById('chat-history-list'); if (sessionsListEl) { sessionsListEl.innerHTML = `

βœ… Session ready

Send a message to start chatting

`; } // Refresh sidebar to show all active sessions if (typeof loadChatHistory === 'function') { setTimeout(() => loadChatHistory(), 100); } else if (typeof window.refreshSessionList === 'function') { setTimeout(() => window.refreshSessionList(), 100); } return; } console.log(`[loadSessionMessages] Loading ${totalMessages} messages in batches of ${BATCH_SIZE}`); // Process first batch immediately const firstBatch = messages.slice(0, BATCH_SIZE); for (const entry of firstBatch) { // Check if entry has role information (newer format) if (entry.role) { appendMessage(entry.role, entry.content, false); } else { // Legacy format - assume assistant if no role specified appendMessage('assistant', entry.content, false); } } // Process remaining batches in deferred chunks if (totalMessages > BATCH_SIZE) { let currentIndex = BATCH_SIZE; function processNextBatch() { const batch = messages.slice(currentIndex, currentIndex + BATCH_SIZE); for (const entry of batch) { // Check if entry has role information (newer format) if (entry.role) { appendMessage(entry.role, entry.content, false); } else { // Legacy format - assume assistant if no role specified appendMessage('assistant', entry.content, false); } } currentIndex += BATCH_SIZE; // Continue with next batch using requestIdleCallback or setTimeout if (currentIndex < totalMessages) { if (window.requestIdleCallback) { window.requestIdleCallback(processNextBatch, { timeout: 50 }); } else { setTimeout(processNextBatch, 0); } } } // Start processing batches if (window.requestIdleCallback) { window.requestIdleCallback(processNextBatch, { timeout: 50 }); } else { setTimeout(processNextBatch, 0); } } } // Refresh sidebar to show all active sessions after loading if (typeof loadChatHistory === 'function') { loadChatHistory(); } else if (typeof window.refreshSessionList === 'function') { window.refreshSessionList(); } } catch (error) { console.error('Error loading session messages:', error); if (window.traceExecution) { window.traceExecution('chat-functions', 'loadSessionMessages ERROR', { sessionId, error: error.message }); } appendSystemMessage('❌ Failed to load messages: ' + error.message); } } // Handle Chat Input (modern input handler) function handleChatInput(event) { const input = event.target; const wrapper = document.getElementById('chat-input-wrapper'); const charCountBadge = document.getElementById('char-count-badge'); // Update character count badge const charCount = input.value.length; charCountBadge.textContent = charCount + ' chars'; // Toggle typing state for badge visibility if (charCount > 0) { wrapper.classList.add('typing'); } else { wrapper.classList.remove('typing'); } // Auto-resize textarea input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 200) + 'px'; // Check for terminal command suggestion in Auto/Native modes if ((currentChatMode === 'auto' || currentChatMode === 'native') && charCount > 0) { checkForTerminalCommand(input.value); } else { hideModeSuggestion(); } } // Handle Chat Key Press function handleChatKeypress(event) { const input = document.getElementById('chat-input'); // Send on Enter (but allow Shift+Enter for new line) if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendChatMessage(); } } // Check for Terminal Command (Task 4: Auto-Suggest Terminal Mode) function checkForTerminalCommand(message) { const banner = document.getElementById('mode-suggestion-banner'); // Don't show suggestion if already in webcontainer mode if (currentChatMode === 'webcontainer') { hideModeSuggestion(); return; } // Check if message looks like a shell command if (isShellCommand(message)) { showModeSuggestion(); } else { hideModeSuggestion(); } } // Show Mode Suggestion Banner function showModeSuggestion() { const banner = document.getElementById('mode-suggestion-banner'); if (banner && banner.style.display === 'none') { banner.style.display = 'flex'; // Auto-hide after 10 seconds if no action if (modeSuggestionTimeout) { clearTimeout(modeSuggestionTimeout); } modeSuggestionTimeout = setTimeout(() => { hideModeSuggestion(); }, 10000); } } // Hide Mode Suggestion Banner function hideModeSuggestion() { const banner = document.getElementById('mode-suggestion-banner'); if (banner && banner.style.display !== 'none') { banner.classList.add('fade-out'); setTimeout(() => { banner.style.display = 'none'; banner.classList.remove('fade-out'); }, 300); if (modeSuggestionTimeout) { clearTimeout(modeSuggestionTimeout); modeSuggestionTimeout = null; } } } // Switch to Terminal Mode function switchToTerminalMode() { hideModeSuggestion(); setChatMode('webcontainer'); appendSystemMessage('βœ… Switched to Terminal mode. Your commands will execute in a persistent terminal session.'); } // Dismiss Mode Suggestion and Send Anyway function dismissModeSuggestion() { hideModeSuggestion(); // Proceed with sending the message in current mode sendChatMessage(); } // Send Chat Message (Enhanced with smart input parsing) // @param {string} messageOverride - Optional message to send instead of input value // @param {string} modeOverride - Optional mode to use instead of current mode // @param {Object} options - Optional settings like { skipValidation: true } async function sendChatMessage(messageOverride, modeOverride, options = {}) { const input = document.getElementById('chat-input'); const message = messageOverride || input.value.trim(); if (!message) return; if (window.traceExecution) { window.traceExecution('chat-functions', 'sendChatMessage START', { messageLength: message.length, messagePreview: message.substring(0, 50), modeOverride, attachedSessionId, hasMessageOverride: !!messageOverride }); } // ============================================================ // RACE CONDITION FIX: Block sending while waiting for attachment // ============================================================ // Don't allow sending messages while we're waiting for session attachment // to complete. This prevents the race condition where a user sends a // message before attachToSession() finishes, which would trigger // startNewChat() and create a wrong session. if (window.pendingSessionAttach && !attachedSessionId) { console.log('[sendChatMessage] ⏳ Blocking message - waiting for session attachment'); appendSystemMessage('⏳ Please wait while the session is being loaded...'); return; } // ============================================================ // SEMANTIC VALIDATION - Detect intent/behavior mismatches // Kimi-style flow: skip validation if explicitly requested (e.g., for approvals) // ============================================================ if (window.semanticValidator && !options.skipValidation) { // Track user message for context window.semanticValidator.trackUserMessage(message); // Get the mode BEFORE any validation const selectedMode = modeOverride || currentChatMode || 'auto'; // IMPORTANT: In Terminal/WebContainer mode, check if this is a command request first // If user says "run ping google.com", we should EXECUTE it, not block it! if (selectedMode === 'webcontainer') { const extractedCommand = window.semanticValidator.extractCommand(message); // If command was extracted from conversational language, allow it through if (extractedCommand !== message) { console.log('[sendChatMessage] Command request detected, allowing execution:', extractedCommand); // Don't return - let the command execute } else { // No extraction, run normal validation const validation = window.semanticValidator.validateIntentBeforeExecution(message, selectedMode); if (!validation.valid && validation.error) { // Report semantic error to bug tracker window.semanticValidator.reportSemanticError(validation.error); // Show user-friendly message based on error type if (validation.error.subtype === 'conversational_as_command') { appendSystemMessage(`πŸ’¬ This looks like a conversational message, not a shell command. You're currently in Terminal mode which executes shell commands. Options: 1. Switch to Chat mode (click "Auto" or "Native" button above) 2. Rephrase as a shell command (e.g., ls -la, npm install) Your message: "${escapeHtml(message.substring(0, 50))}${message.length > 50 ? '...' : ''}"`); // Auto-switch to Chat mode after delay setTimeout(() => { if (currentChatMode === 'webcontainer') { setChatMode('auto'); appendSystemMessage('βœ… Switched to Chat mode. You can continue your conversation.'); } }, 4000); clearInput(); hideStreamingIndicator(); setGeneratingState(false); return; } if (validation.error.subtype === 'approval_loop') { appendSystemMessage(`⚠️ Intent Mismatch Detected The AI assistant asked for your approval, but you responded in Terminal mode which executes commands. What happened: β€’ AI: "${validation.error.details.lastAssistantMessage || 'Asked for permission'}" β€’ You: "${escapeHtml(message)}" β€’ System: Tried to execute "${escapeHtml(message)}" as a command Suggested fix: Switch to Chat mode for conversational interactions.`); clearInput(); hideStreamingIndicator(); setGeneratingState(false); return; } } } } else { // In Chat/Auto mode, run normal validation const validation = window.semanticValidator.validateIntentBeforeExecution(message, selectedMode); if (!validation.valid && validation.error) { // Report semantic error to bug tracker window.semanticValidator.reportSemanticError(validation.error); // Show user-friendly message based on error type if (validation.error.subtype === 'conversational_as_command') { appendSystemMessage(`πŸ’¬ This looks like a conversational message, not a shell command. You're currently in Terminal mode which executes shell commands. Options: 1. Switch to Chat mode (click "Auto" or "Native" button above) 2. Rephrase as a shell command (e.g., ls -la, npm install) Your message: "${escapeHtml(message.substring(0, 50))}${message.length > 50 ? '...' : ''}"`); // Auto-switch to Chat mode after delay setTimeout(() => { if (currentChatMode === 'webcontainer') { setChatMode('auto'); appendSystemMessage('βœ… Switched to Chat mode. You can continue your conversation.'); } }, 4000); clearInput(); hideStreamingIndicator(); setGeneratingState(false); return; } if (validation.error.subtype === 'approval_loop') { appendSystemMessage(`⚠️ Intent Mismatch Detected The AI assistant asked for your approval, but you responded in Terminal mode which executes commands. What happened: β€’ AI: "${validation.error.details.lastAssistantMessage || 'Asked for permission'}" β€’ You: "${escapeHtml(message)}" β€’ System: Tried to execute "${escapeHtml(message)}" as a command Suggested fix: Switch to Chat mode for conversational interactions.`); clearInput(); hideStreamingIndicator(); setGeneratingState(false); return; } } } } // Auto-create session if none exists (OpenCode/CodeNomad hybrid approach) if (!attachedSessionId) { console.log('[sendChatMessage] No session attached, auto-creating...'); appendSystemMessage('Creating new session...'); try { await startNewChat(); // After session creation, wait a moment for attachment await new Promise(resolve => setTimeout(resolve, 500)); // Verify session was created and attached if (!attachedSessionId) { appendSystemMessage('❌ Failed to create session. Please try again.'); return; } console.log('[sendChatMessage] Session auto-created:', attachedSessionId); } catch (error) { console.error('[sendChatMessage] Auto-create session failed:', error); appendSystemMessage('❌ Failed to create session: ' + error.message); return; } } // Hide mode suggestion banner hideModeSuggestion(); // Parse smart input (file references, commands) let parsed = null; if (typeof smartInput !== 'undefined' && smartInput) { try { parsed = smartInput.parser.parse(message); // Update context panel with referenced files if (parsed.files.length > 0 && typeof contextPanel !== 'undefined' && contextPanel) { parsed.files.forEach(filePath => { const fileName = filePath.split('/').pop(); const ext = fileName.split('.').pop(); const icon = getFileIcon(ext); contextPanel.addActiveFile(filePath, fileName, icon); }); console.log('Added', parsed.files.length, 'referenced files to context panel'); } console.log('Parsed input:', parsed); } catch (error) { console.error('Error parsing smart input:', error); } } // Use selected mode from buttons, or fall back to parsed mode, or override const selectedMode = modeOverride || currentChatMode || 'auto'; // Add user message to chat (but only if it's from user input, not programmatic) if (!messageOverride) { appendMessage('user', message); clearInput(); } else { // For programmatic messages, still show them but don't clear input appendMessage('user', message); } // Show streaming indicator and update button state showStreamingIndicator(); setGeneratingState(true); // Handle WebContainer mode separately if (selectedMode === 'webcontainer') { await handleWebContainerCommand(message); return; } // ============================================================ // HYBRID APPROACH: Send commands via REST API instead of WebSocket // ============================================================ // SSE is for receiving events only. Commands are sent via REST API. try { console.log('[sendChatMessage] Sending command via REST API to session:', attachedSessionId); // Prepare request body const requestBody = { command: message, mode: selectedMode }; // Add metadata if available (files, commands) const hasFiles = parsed && parsed.files.length > 0; const hasCommands = parsed && parsed.commands.length > 0; if (hasFiles || hasCommands) { requestBody.metadata = { files: parsed ? parsed.files : [], commands: parsed ? parsed.commands : [], hasFileReferences: parsed ? parsed.hasFileReferences : false }; console.log('[sendChatMessage] Sending with metadata:', requestBody.metadata); } // Send via REST API (use /claude/api prefix for production nginx routing) // NOTE: Use /claude/api/claude/sessions/:sessionId/prompt to access sessions-routes.js // which has historical session auto-recreate logic const apiUrl = `/claude/api/claude/sessions/${encodeURIComponent(attachedSessionId)}/prompt`; if (window.traceExecution) { window.traceExecution('chat-functions', 'sendChatMessage - calling API', { sessionId: attachedSessionId, apiUrl, requestBody: { commandLength: requestBody.command?.length, mode: requestBody.mode } }); } const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); console.log('[sendChatMessage] Response status:', response.status); if (window.traceExecution) { window.traceExecution('chat-functions', 'sendChatMessage - API response', { status: response.status, ok: response.ok }); } if (!response.ok) { const errorText = await response.text(); console.error('[sendChatMessage] API error:', response.status, errorText); throw new Error(`HTTP ${response.status}: ${errorText}`); } const result = await response.json(); console.log('[sendChatMessage] Command sent successfully:', result); // ============================================================ // AUTO-RECREATE HANDLING: Check if backend created a new session // ============================================================ if (result.newSession && result.sessionId !== attachedSessionId) { console.log('[sendChatMessage] πŸ”„ Session was auto-recreated, switching to new session:', result.sessionId); // Update session IDs const oldSessionId = attachedSessionId; attachedSessionId = result.sessionId; chatSessionId = result.sessionId; // Update UI document.getElementById('current-session-id').textContent = result.sessionId; // Reconnect SSE to new session if (window.sseClient) { console.log('[sendChatMessage] Reconnecting SSE to new session:', result.sessionId); window.sseClient.disconnect(); window.sseClient.connect(result.sessionId); } // Update URL without page reload const newUrl = `/claude/ide/session/${result.sessionId}`; window.history.replaceState({ sessionId: result.sessionId }, '', newUrl); appendSystemMessage(`βœ… Switched to new session (${result.sessionId.substring(-8)})`); } // Note: The actual response will come via SSE events // The REST API just confirms the command was queued } catch (error) { console.error('[sendChatMessage] Error sending message:', error); hideStreamingIndicator(); setGeneratingState(false); appendSystemMessage('Failed to send message: ' + error.message); } } // Task 1: Set Generating State (show/hide stop button) function setGeneratingState(generating) { isGenerating = generating; const sendButton = document.getElementById('send-button'); const stopButton = document.getElementById('stop-button'); if (generating) { // Show stop button, hide send button if (sendButton) sendButton.classList.add('hidden'); if (stopButton) stopButton.classList.remove('hidden'); } else { // Show send button, hide stop button if (sendButton) sendButton.classList.remove('hidden'); if (stopButton) stopButton.classList.add('hidden'); } } // Task 1: Stop Generation function stopGeneration() { console.log('Stopping generation...'); if (!window.ws || window.ws.readyState !== WebSocket.OPEN) { appendSystemMessage('⚠️ Cannot stop: WebSocket not connected'); return; } // Send stop signal via WebSocket window.ws.send(JSON.stringify({ type: 'stop', sessionId: attachedSessionId })); appendSystemMessage('⏸️ Stopping generation...'); // Update UI state hideStreamingIndicator(); setGeneratingState(false); } // Helper function to get file icon for context panel function getFileIcon(ext) { const icons = { 'js': 'πŸ“œ', 'mjs': 'πŸ“œ', 'ts': 'πŸ“˜', 'tsx': 'βš›οΈ', 'jsx': 'βš›οΈ', 'html': '🌐', 'htm': '🌐', 'css': '🎨', 'scss': '🎨', 'py': '🐍', 'rb': 'πŸ’Ž', 'php': '🐘', 'json': 'πŸ“‹', 'xml': 'πŸ“„', 'md': 'πŸ“', 'txt': 'πŸ“„', 'png': 'πŸ–ΌοΈ', 'jpg': 'πŸ–ΌοΈ', 'jpeg': 'πŸ–ΌοΈ', 'gif': 'πŸ–ΌοΈ', 'svg': 'πŸ–ΌοΈ', 'sh': 'πŸ–₯️', 'bash': 'πŸ–₯️', 'zsh': 'πŸ–₯️', 'yml': 'βš™οΈ', 'yaml': 'βš™οΈ', 'toml': 'βš™οΈ' }; return icons[ext] || 'πŸ“„'; } // Chat Mode Management let currentChatMode = 'auto'; function setChatMode(mode) { currentChatMode = mode; // Update button states document.querySelectorAll('.mode-btn').forEach(btn => { btn.classList.remove('active'); if (btn.dataset.mode === mode) { btn.classList.add('active'); } }); // Update context panel with mode if (typeof contextPanel !== 'undefined' && contextPanel) { contextPanel.setMode(mode); } // Initialize WebContainer if switching to webcontainer mode if (mode === 'webcontainer') { initializeWebContainer(); } // Show mode change message const modeNames = { 'auto': 'πŸ€– Auto (best mode will be detected automatically)', 'native': 'πŸ’» Native (commands execute directly on your system)', 'webcontainer': 'πŸ’» Terminal (executes commands in persistent terminal session)' }; appendSystemMessage(`Execution mode changed to: ${modeNames[mode]}`); console.log('Chat mode set to:', mode); } // Initialize WebContainer for current session async function initializeWebContainer() { try { if (typeof webContainerManager === 'undefined') { appendSystemMessage('⚠️ WebContainer Manager not loaded. Refresh the page.'); return; } // Check if already initialized for current session const sessionId = attachedSessionId || chatSessionId; if (!sessionId) { appendSystemMessage('⚠️ Please start a chat session first.'); return; } const status = webContainerManager.getStatus(); if (status.initialized && status.currentSession === sessionId) { appendSystemMessage('βœ… WebContainer already initialized for this session'); return; } appendSystemMessage('πŸ”„ Initializing WebContainer environment...'); await webContainerManager.initialize(sessionId); appendSystemMessage('βœ… WebContainer ready! Commands will execute in browser sandbox.'); } catch (error) { console.error('Failed to initialize WebContainer:', error); appendSystemMessage('❌ Failed to initialize WebContainer: ' + error.message); } } // Detect if message is a shell command (using semantic validator) function isShellCommand(message) { if (window.semanticValidator) { return window.semanticValidator.isShellCommand(message); } // Fallback to basic detection if validator not loaded console.warn('[SemanticValidator] Not loaded, using basic command detection'); const trimmed = message.trim().toLowerCase(); // Basic conversational check const conversationalPatterns = [ /^(if|when|where|what|how|why|who|which|whose|can|could|would|should|will|do|does|did|is|are|was|were|am|have|has|had|please|thank|hey|hello|hi)\s/i, /^(i|you|he|she|it|we|they)\s/i, /^(yes|no|maybe|ok|okay|sure|alright)\s/i ]; if (conversationalPatterns.some(pattern => pattern.test(trimmed))) { return false; } // Basic command patterns const basicPatterns = [ /^(cd|ls|pwd|echo|cat|grep|find|rm|cp|mv|mkdir|npm|git|ping|curl|wget|node|python)(\s|$)/, /^\//, /^[a-z0-9_\-./]+\s*[\|>]/ ]; return basicPatterns.some(pattern => pattern.test(trimmed)); } // Send shell command to active Claude CLI session via WebSocket async function sendShellCommand(sessionId, command) { try { // Use WebSocket to send shell command (backend will execute through native mode) if (!window.ws || window.ws.readyState !== WebSocket.OPEN) { throw new Error('WebSocket not connected'); } // Send command via WebSocket window.ws.send(JSON.stringify({ type: 'command', sessionId: sessionId, command: command, metadata: { executionMode: 'native', timestamp: new Date().toISOString() } })); // Return a promise that will be resolved when the command completes // Note: The actual output will come through WebSocket messages return { success: true, message: 'Command sent via WebSocket' }; } catch (error) { console.error('[Shell Command] Error:', error); throw error; } } // Handle command execution in Full Stack mode (via Claude CLI session's stdin) async function handleWebContainerCommand(message) { const sessionId = attachedSessionId || chatSessionId; if (!sessionId) { appendSystemMessage('⚠️ No active session.'); hideStreamingIndicator(); setGeneratingState(false); return; } // Smart command detection if (!isShellCommand(message)) { hideStreamingIndicator(); setGeneratingState(false); appendSystemMessage(`πŸ’¬ This looks like a conversational message, not a shell command. Terminal mode executes commands directly. For example: β€’ npm install β€’ ls -la β€’ git status β€’ python script.py Would you like to: 1. Switch to Chat mode for conversational AI 2. Rephrase as a shell command`); // Auto-switch to Chat mode after brief delay setTimeout(() => { if (currentChatMode === 'webcontainer') { setChatMode('auto'); appendSystemMessage('βœ… Switched to Chat mode. You can continue your conversation.'); } }, 3000); return; } // Extract actual command if embedded in conversational language let actualCommand = message; if (window.semanticValidator && typeof window.semanticValidator.extractCommand === 'function') { const extracted = window.semanticValidator.extractCommand(message); if (extracted && extracted !== message) { actualCommand = extracted; appendSystemMessage(`🎯 Detected command request: "${escapeHtml(actualCommand)}"`); } } // Track command execution const commandId = window.commandTracker ? window.commandTracker.startCommand(message, currentChatMode, sessionId) : null; try { appendSystemMessage(`πŸ’» Executing in session: ${escapeHtml(actualCommand)}`); // Send shell command to the active Claude CLI session via WebSocket // Note: Output will be received asynchronously via WebSocket messages await sendShellCommand(sessionId, actualCommand); // Store command ID for later completion if (commandId) { window._pendingCommandId = commandId; } // Don't hide indicators yet - wait for actual output via WebSocket // The output will be displayed through handleSessionOutput() // Record tool usage if (typeof contextPanel !== 'undefined' && contextPanel) { contextPanel.recordToolUsage('shell_command'); } } catch (error) { console.error('Shell command execution failed:', error); // Mark command as failed in tracker if (commandId && window.commandTracker) { window.commandTracker.completeCommand(commandId, null, error.message); } hideStreamingIndicator(); setGeneratingState(false); appendSystemMessage('❌ Failed to execute command: ' + error.message); } } // Fallback: Execute command in native mode via WebSocket async function executeNativeCommand(message, sessionId) { try { // Send via WebSocket (backend will execute through native mode) window.ws.send(JSON.stringify({ type: 'command', sessionId: sessionId, command: message, metadata: { executionMode: 'native', timestamp: new Date().toISOString() } })); appendSystemMessage('βœ… Command sent in native execution mode'); } catch (error) { console.error('Native execution failed:', error); throw new Error('Failed to execute in native mode: ' + error.message); } } // Append Message to Chat (Enhanced with OpenCode-style rendering) function appendMessage(role, content, scroll) { const messagesContainer = document.getElementById('chat-messages'); if (window.traceExecution) { window.traceExecution('chat-functions', 'appendMessage', { role, contentLength: content?.length || 0, contentPreview: typeof content === 'string' ? content.substring(0, 100) : '[non-string content]', scroll, existingMessages: messagesContainer?.children?.length || 0 }); } // Remove welcome message if present const welcome = messagesContainer.querySelector('.chat-welcome'); if (welcome) { welcome.remove(); } // Remove streaming indicator if present const streaming = messagesContainer.querySelector('.streaming-indicator'); if (streaming) { streaming.remove(); } // Use enhanced message system if available, otherwise fall back to basic if (typeof ChatMessage !== 'undefined' && typeof renderEnhancedMessage !== 'undefined') { // Create enhanced message with part-based structure const message = new ChatMessage(role, [ new MessagePart('text', content) ]); const messageDiv = renderEnhancedMessage(message); messagesContainer.appendChild(messageDiv); } else { // Fallback to basic rendering const messageDiv = document.createElement('div'); messageDiv.className = 'chat-message ' + role; const avatar = document.createElement('div'); avatar.className = 'chat-message-avatar'; avatar.textContent = role === 'user' ? 'πŸ‘€' : 'πŸ€–'; const contentDiv = document.createElement('div'); contentDiv.className = 'chat-message-content'; const bubble = document.createElement('div'); bubble.className = 'chat-message-bubble'; // Format content (handle code blocks, etc.) bubble.innerHTML = formatMessage(content); const timestamp = document.createElement('div'); timestamp.className = 'chat-message-timestamp'; timestamp.textContent = new Date().toLocaleTimeString(); contentDiv.appendChild(bubble); contentDiv.appendChild(timestamp); messageDiv.appendChild(avatar); messageDiv.appendChild(contentDiv); messagesContainer.appendChild(messageDiv); } // Scroll to bottom if (scroll || scroll === undefined) { messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Update token usage (estimated) updateTokenUsage(content.length); } // Append System Message function appendSystemMessage(text) { const messagesContainer = document.getElementById('chat-messages'); // Remove welcome message if present const welcome = messagesContainer.querySelector('.chat-welcome'); if (welcome) { welcome.remove(); } const systemDiv = document.createElement('div'); systemDiv.className = 'chat-message assistant'; systemDiv.style.opacity = '0.8'; const avatar = document.createElement('div'); avatar.className = 'chat-message-avatar'; avatar.textContent = 'ℹ️'; const contentDiv = document.createElement('div'); contentDiv.className = 'chat-message-content'; const bubble = document.createElement('div'); bubble.className = 'chat-message-bubble'; bubble.innerHTML = '' + escapeHtml(text) + ''; contentDiv.appendChild(bubble); systemDiv.appendChild(avatar); systemDiv.appendChild(contentDiv); messagesContainer.appendChild(systemDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Format Message (handle code blocks, markdown, etc.) function formatMessage(content) { // Escape HTML first let formatted = escapeHtml(content); // Handle code blocks formatted = formatted.replace(/```(\w+)?\n([\s\S]*?)```/g, function(match, lang, code) { return '
' + code.trim() + '
'; }); // Handle inline code formatted = formatted.replace(/`([^`]+)`/g, '$1'); // Handle line breaks formatted = formatted.replace(/\n/g, '
'); return formatted; } // Show Streaming Indicator function showStreamingIndicator() { const messagesContainer = document.getElementById('chat-messages'); // Remove existing streaming indicator const existing = messagesContainer.querySelector('.streaming-indicator'); if (existing) { existing.remove(); } const streamingDiv = document.createElement('div'); streamingDiv.className = 'streaming-indicator'; streamingDiv.innerHTML = '
'; messagesContainer.appendChild(streamingDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Hide Streaming Indicator function hideStreamingIndicator() { const streaming = document.querySelector('.streaming-indicator'); if (streaming) { streaming.remove(); } // Also reset generating state setGeneratingState(false); } // Clear Chat Display function clearChatDisplay() { const messagesContainer = document.getElementById('chat-messages'); messagesContainer.innerHTML = ''; // Reset token usage document.getElementById('token-usage').textContent = '0 tokens used'; } // Clear Chat function clearChat() { if (confirm('Clear all messages in this chat?')) { clearChatDisplay(); // Show welcome message again const messagesContainer = document.getElementById('chat-messages'); messagesContainer.innerHTML = `

πŸ‘‹ Chat Cleared

Start a new conversation with Claude Code.

`; } } // Clear Input function clearInput() { const input = document.getElementById('chat-input'); const wrapper = document.getElementById('chat-input-wrapper'); const charCountBadge = document.getElementById('char-count-badge'); if (input) { input.value = ''; input.style.height = 'auto'; } if (wrapper) { wrapper.classList.remove('typing'); } if (charCountBadge) { charCountBadge.textContent = '0 chars'; } } // Update Token Usage function updateTokenUsage(charCount) { // Rough estimation: ~4 characters per token const estimatedTokens = Math.ceil(charCount / 4); const currentUsage = parseInt(document.getElementById('token-usage').textContent) || 0; document.getElementById('token-usage').textContent = (currentUsage + estimatedTokens) + ' tokens used'; } // Show Attach CLI Modal function showAttachCliModal() { document.getElementById('modal-overlay').classList.remove('hidden'); document.getElementById('attach-cli-modal').classList.remove('hidden'); } // Submit Attach CLI Session async function submitAttachCliSession() { const sessionId = document.getElementById('cli-session-id').value.trim(); if (!sessionId) { alert('Please enter a session ID'); return; } attachToSession(sessionId); closeModal(); } // Attach File (placeholder for now) function attachFile() { appendSystemMessage('File attachment feature coming soon! For now, use @filename to reference files.'); } // Attach Image (placeholder for now) function attachImage() { appendSystemMessage('Image attachment feature coming soon! For now, use @filename to reference image files.'); } // Insert Code Snippet (placeholder for now) function insertCodeSnippet() { const input = document.getElementById('chat-input'); const snippet = '```\n// Your code here\n```\n'; input.value += snippet; input.focus(); handleChatInput({ target: input }); } // Show Chat Settings (placeholder) function showChatSettings() { appendSystemMessage('Chat settings coming soon!'); } // Export variables to window for global access if (typeof window !== 'undefined') { window.attachedSessionId = attachedSessionId; window.chatSessionId = chatSessionId; window.chatMessages = chatMessages; // Create a proxy to keep window vars in sync Object.defineProperty(window, 'attachedSessionId', { get: function() { return attachedSessionId; }, set: function(value) { attachedSessionId = value; } }); Object.defineProperty(window, 'chatSessionId', { get: function() { return chatSessionId; }, set: function(value) { chatSessionId = value; } }); // ============================================================ // SSE: Register event handlers after page load // ============================================================ // Extract sessionId from URL path directly and register handlers const registerSSEHandler = () => { // Extract sessionId from URL path: /claude/ide/session/{sessionId} const pathMatch = window.location.pathname.match(/\/claude\/ide\/session\/([^/]+)$/); if (pathMatch && pathMatch[1]) { const sessionId = decodeURIComponent(pathMatch[1]); console.log('[chat-functions] Registering SSE handlers for session from URL:', sessionId); registerSSEEventHandlers(sessionId); } }; // Register when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(registerSSEHandler, 100); }); } else { // DOM already loaded, register immediately setTimeout(registerSSEHandler, 100); } }