Files
SuperCharged-Claude-Code-Up…/public/claude-ide/chat-functions.js
uroma 3067c6bc24 Fix two critical session issues in Claude Code IDE
Issue 1: Sessions history not showing in left sidebar
- Converted loadChatHistoryOnLoad IIFE to named loadChatHistory() function
- Added refresh calls in loadSessionMessages() after loading messages
- Added guard to skip refresh if showing "Loading session..." state
- Sidebar now properly shows all active sessions after attachment

Issue 2: New chat session button fails with 'Failed to create session'
- Changed startNewChat() to call loadChatHistory() instead of loadChatView()
- Prevents triggering URL-based attachment logic that was causing confusion
- Sidebar now refreshes correctly without getting stuck in loading state

Also updated cache-bust version to force browser reload.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 11:46:31 +00:00

1925 lines
75 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================
// 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 = `
<div class="chat-history-empty">
<p>Loading session: <strong>${pendingSessionId.substring(0, 20)}...</strong></p>
<div class="loading-spinner" style="margin: 20px auto;"></div>
</div>
`;
// 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 `
<div class="chat-history-item ${session.id === attachedSessionId ? 'active' : ''}"
onclick="attachToSession('${session.id}')">
<div class="chat-history-icon">💬</div>
<div class="chat-history-content">
<div class="chat-history-title">${projectName}</div>
<div class="chat-history-meta">
<span class="chat-history-date">${new Date(session.createdAt).toLocaleDateString()}</span>
<span class="chat-history-status active">Running</span>
</div>
</div>
</div>
`;
}).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 = `
<div class="chat-history-empty">
<p>Creating session for project: <strong>${currentProjectName}</strong></p>
<div class="loading-spinner" style="margin: 20px auto;"></div>
</div>
`;
// Auto-create session
startNewChat();
} else {
const emptyMessage = `<p>No active sessions</p>`;
sessionsListEl.innerHTML = `
<div class="chat-history-empty">
${emptyMessage}
<button class="btn-primary" onclick="startNewChat()" style="margin-top: 12px;">+ Start New Chat</button>
</div>
`;
}
}
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 = `
<div class="chat-history-empty">
<p>Error: ${error.message}</p>
<button class="btn-secondary" onclick="loadChatView()" style="margin-top: 12px;">Retry</button>
</div>
`;
}
}
}
/**
* 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 = `
<div class="chat-history-empty">
<p style="color: #51cf66;">✅ Session ready</p>
<p style="font-size: 13px; color: #888;">Send a message to start chatting</p>
</div>
`;
}
}, 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 = `
<div class="chat-history-empty">
<p style="color: #51cf66;">✅ Session ready</p>
<p style="font-size: 13px; color: #888;">Send a message to start chatting</p>
</div>
`;
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 `
<div class="chat-history-item ${isActive}"
onclick="attachToSession('${session.id}')">
<div class="chat-history-icon">💬</div>
<div class="chat-history-content">
<div class="chat-history-title">${projectName}</div>
<div class="chat-history-meta">
<span class="chat-history-date">${new Date(session.createdAt).toLocaleDateString()}</span>
<span class="chat-history-status active">Running</span>
</div>
</div>
</div>
`;
}).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 = `
<div class="chat-history-empty">
<p style="color: #51cf66;">✅ Session ready</p>
<p style="font-size: 13px; color: #888;">Send a message to start chatting</p>
</div>
`;
}
// 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 <strong>Terminal mode</strong> which executes shell commands.
<strong>Options:</strong>
1. Switch to Chat mode (click "Auto" or "Native" button above)
2. Rephrase as a shell command (e.g., <code>ls -la</code>, <code>npm install</code>)
<strong>Your message:</strong> "${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(`⚠️ <strong>Intent Mismatch Detected</strong>
The AI assistant asked for your approval, but you responded in <strong>Terminal mode</strong> which executes commands.
<strong>What happened:</strong>
• AI: "${validation.error.details.lastAssistantMessage || 'Asked for permission'}"
• You: "${escapeHtml(message)}"
• System: Tried to execute "${escapeHtml(message)}" as a command
<strong>Suggested fix:</strong> 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 <strong>Terminal mode</strong> which executes shell commands.
<strong>Options:</strong>
1. Switch to Chat mode (click "Auto" or "Native" button above)
2. Rephrase as a shell command (e.g., <code>ls -la</code>, <code>npm install</code>)
<strong>Your message:</strong> "${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(`⚠️ <strong>Intent Mismatch Detected</strong>
The AI assistant asked for your approval, but you responded in <strong>Terminal mode</strong> which executes commands.
<strong>What happened:</strong>
• AI: "${validation.error.details.lastAssistantMessage || 'Asked for permission'}"
• You: "${escapeHtml(message)}"
• System: Tried to execute "${escapeHtml(message)}" as a command
<strong>Suggested fix:</strong> 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:
• <code>npm install</code>
• <code>ls -la</code>
• <code>git status</code>
• <code>python script.py</code>
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: <code>${escapeHtml(actualCommand)}</code>`);
// 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 = '<em>' + escapeHtml(text) + '</em>';
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 '<pre><code class="language-' + (lang || 'text') + '">' + code.trim() + '</code></pre>';
});
// Handle inline code
formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>');
// Handle line breaks
formatted = formatted.replace(/\n/g, '<br>');
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 = '<div class="streaming-dot"></div><div class="streaming-dot"></div><div class="streaming-dot"></div>';
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 = `
<div class="chat-welcome">
<h2>👋 Chat Cleared</h2>
<p>Start a new conversation with Claude Code.</p>
</div>
`;
}
}
// 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);
}
}