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