// ============================================ // 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...'); // Check if there's a pending session from Sessions view if (window.pendingSessionId) { console.log('[loadChatView] Detected pending session:', 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 let activeSessions = (data.active || []).filter(s => s.status === 'running'); 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); 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 { // Zero-friction entry: Auto-create session in project context if (currentProjectName && window.currentProjectDir) { 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 */ 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) if (sessionData.outputBuffer && sessionData.outputBuffer.length > 0) { sessionData.outputBuffer.forEach(entry => { if (entry.role) { appendMessage(entry.role, entry.content, false); } else { // Legacy format - default to assistant appendMessage('assistant', entry.content, false); } }); } // Show success message 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(sessionId); } // Focus input for active sessions if (isRunning) { setTimeout(() => { const input = document.getElementById('chat-input'); if (input) input.focus(); }, 100); } } catch (error) { console.error('[loadSessionIntoChat] Error:', error); appendSystemMessage('❌ Failed to load session: ' + error.message); } } // 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)); await loadChatView().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) { attachedSessionId = sessionId; chatSessionId = sessionId; // 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); // Subscribe to session via WebSocket subscribeToSession(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); } // Subscribe to session via WebSocket function subscribeToSession(sessionId) { if (window.ws && window.ws.readyState === WebSocket.OPEN) { window.ws.send(JSON.stringify({ type: 'subscribe', sessionId: sessionId })); console.log('Subscribed to session:', 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 async function loadSessionMessages(sessionId) { try { const res = await fetch('/claude/api/claude/sessions/' + sessionId); const data = await res.json(); if (data.session) { clearChatDisplay(); // Add existing messages from output buffer - restore both user and assistant messages data.session.outputBuffer.forEach(entry => { // 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); } }); } } catch (error) { console.error('Error loading session messages:', error); } } // 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) async function sendChatMessage() { const input = document.getElementById('chat-input'); const message = input.value.trim(); if (!message) return; // ============================================================ // SEMANTIC VALIDATION - Detect intent/behavior mismatches // ============================================================ if (window.semanticValidator) { // Track user message for context window.semanticValidator.trackUserMessage(message); // Get the mode BEFORE any validation const selectedMode = 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 const selectedMode = currentChatMode || 'auto'; // Add user message to chat appendMessage('user', message); clearInput(); // Show streaming indicator and update button state showStreamingIndicator(); setGeneratingState(true); // Handle WebContainer mode separately if (selectedMode === 'webcontainer') { await handleWebContainerCommand(message); return; } try { // Check WebSocket state if (!window.ws) { console.error('WebSocket is null/undefined'); appendSystemMessage('WebSocket not initialized. Please refresh the page.'); hideStreamingIndicator(); setGeneratingState(false); return; } const state = window.ws.readyState; const stateName = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][state] || 'UNKNOWN'; console.log('WebSocket state:', state, stateName); if (state !== WebSocket.OPEN) { console.error('WebSocket not in OPEN state:', stateName); appendSystemMessage(`WebSocket not ready (state: ${stateName}). Retrying...`); hideStreamingIndicator(); setGeneratingState(false); // Trigger reconnection if closed if (state === WebSocket.CLOSED) { console.log('WebSocket closed, triggering reconnection...'); if (typeof connectWebSocket === 'function') { connectWebSocket(); } } return; } // Send command via WebSocket with parsed metadata const payload = { type: 'command', sessionId: attachedSessionId, command: message }; // Add metadata if available (files, commands, mode) const hasFiles = parsed && parsed.files.length > 0; const hasCommands = parsed && parsed.commands.length > 0; const modeNotAuto = selectedMode !== 'auto'; if (hasFiles || hasCommands || modeNotAuto) { payload.metadata = { files: parsed ? parsed.files : [], commands: parsed ? parsed.commands : [], mode: selectedMode, hasFileReferences: parsed ? parsed.hasFileReferences : false }; console.log('Sending with metadata:', payload.metadata); } // Debug logging before sending console.log('[DEBUG] About to send command payload:', { type: payload.type, sessionId: payload.sessionId, commandLength: payload.command?.length, wsReady: window.wsReady, wsState: window.ws?.readyState, queueLength: window.messageQueue?.length || 0 }); // Use message queue to prevent race conditions if (typeof queueMessage === 'function') { queueMessage(payload); console.log('[DEBUG] Message queued, queue length now:', window.messageQueue?.length); } else { window.ws.send(JSON.stringify(payload)); console.log('[DEBUG] Sent directly via WebSocket (no queue function)'); } console.log('Sent command via WebSocket:', message.substring(0, 50)); } catch (error) { console.error('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'); // 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; } }); }