// ============================================ // 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 `
Creating session for project: ${currentProjectName}
No active sessions
`; sessionsListEl.innerHTML = `Error: ${error.message}
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, 'Start a new conversation with Claude Code.