diff --git a/.agent/scratchpad.md b/.agent/scratchpad.md
new file mode 100644
index 00000000..402750ee
--- /dev/null
+++ b/.agent/scratchpad.md
@@ -0,0 +1,17 @@
+# Session Fixes - Scratchpad
+
+## Task Overview
+Fix two critical issues in Claude Code IDE:
+1. Sessions history not showing in left sidebar after attaching to a session
+2. New chat session button fails with 'Failed to create session'
+
+## Environment
+- Server: /home/uroma/obsidian-web-interface/server.js (PID 1736251)
+- Frontend: /home/uroma/obsidian-web-interface/public/claude-ide/
+- Session URL: https://rommark.dev/claude/ide/session/session-1769081956055-str90u48t
+
+## Iteration 1 - Current State
+- Created scratchpad
+- Need to explore codebase structure
+- Need to check server logs for API errors
+- Need to verify session creation endpoint
diff --git a/public/claude-ide/chat-enhanced.js b/public/claude-ide/chat-enhanced.js
index ce05f8d0..dd635a34 100644
--- a/public/claude-ide/chat-enhanced.js
+++ b/public/claude-ide/chat-enhanced.js
@@ -47,7 +47,8 @@ function enhanceChatInput() {
// ============================================
// Auto-load chat history when page loads
-(async function loadChatHistoryOnLoad() {
+// Make this a named function so it can be called to refresh the sidebar
+async function loadChatHistory() {
try {
const res = await fetch('/claude/api/claude/sessions');
const data = await res.json();
@@ -55,6 +56,13 @@ function enhanceChatInput() {
const historyList = document.getElementById('chat-history-list');
if (!historyList) return;
+ // Skip update if we're showing "Loading session..." to avoid conflicts
+ // with attachToSession's loading state
+ if (historyList.textContent.includes('Loading session')) {
+ console.log('[loadChatHistory] Skipping update - showing loading state');
+ return;
+ }
+
// Combine active and historical sessions
const allSessions = [
...(data.active || []).map(s => ({...s, status: 'active'})),
@@ -97,9 +105,17 @@ function enhanceChatInput() {
}).join('');
} catch (error) {
- console.error('[loadChatHistoryOnLoad] Error loading chat history:', error);
+ console.error('[loadChatHistory] Error loading chat history:', error);
}
-})();
+}
+
+// Auto-load chat history when page loads
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', loadChatHistory);
+} else {
+ // DOM already loaded, load immediately
+ loadChatHistory();
+}
// Resume historical session
async function resumeSession(sessionId) {
diff --git a/public/claude-ide/chat-functions.js b/public/claude-ide/chat-functions.js
index ced17055..5f06cd7c 100644
--- a/public/claude-ide/chat-functions.js
+++ b/public/claude-ide/chat-functions.js
@@ -21,9 +21,20 @@ function resetChatState() {
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;
@@ -75,7 +86,9 @@ async function loadChatView() {
// 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');
+ // 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
@@ -117,6 +130,75 @@ async function loadChatView() {
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 = `
+
@@ -177,6 +260,7 @@ async function loadChatView() {
/**
* 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 {
@@ -207,42 +291,75 @@ async function loadSessionIntoChat(sessionId, sessionData = null) {
clearChatDisplay();
// Load session messages (both user and assistant)
+ // IMPORTANT: Process messages in batches to prevent blocking
if (sessionData.outputBuffer && sessionData.outputBuffer.length > 0) {
- sessionData.outputBuffer.forEach(entry => {
+ 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);
}
- });
- }
+ }
- // Show success message
- const isRunning = sessionData.status === 'running';
- const statusText = isRunning ? 'Active session' : 'Historical session';
- appendSystemMessage(`✅ Loaded ${statusText} from ${new Date(sessionData.createdAt).toLocaleString()}`);
+ // Process remaining batches in deferred chunks
+ if (totalMessages > BATCH_SIZE) {
+ let currentIndex = BATCH_SIZE;
- if (!isRunning) {
- appendSystemMessage('ℹ️ This is a historical session. Messages are read-only.');
- }
+ function processNextBatch() {
+ const batch = messages.slice(currentIndex, currentIndex + BATCH_SIZE);
- // Update chat history sidebar to highlight this session
- if (typeof loadChatHistory === 'function') {
- loadChatHistory();
- }
+ 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);
+ }
+ }
- // Subscribe to session for live updates (if running)
- if (isRunning) {
- subscribeToSession(sessionId);
- }
+ currentIndex += BATCH_SIZE;
- // Focus input for active sessions
- if (isRunning) {
- setTimeout(() => {
- const input = document.getElementById('chat-input');
- if (input) input.focus();
- }, 100);
+ // 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) {
@@ -251,6 +368,37 @@ async function loadSessionIntoChat(sessionId, sessionData = null) {
}
}
+/**
+ * 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
@@ -308,7 +456,12 @@ async function startNewChat() {
// Give backend time to persist session, then refresh sidebar
// This ensures the new session appears in the list
await new Promise(resolve => setTimeout(resolve, 150));
- await loadChatView().catch(err => console.error('[startNewChat] Background refresh failed:', err));
+ // 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(() => {
@@ -334,9 +487,45 @@ async function startNewChat() {
// 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;
@@ -348,8 +537,35 @@ function attachToSession(sessionId) {
// Load session messages
loadSessionMessages(sessionId);
- // Subscribe to session via WebSocket
- subscribeToSession(sessionId);
+ // Safety timeout: Clear loading state if stuck after 3 seconds
+ setTimeout(() => {
+ const sessionsListEl = document.getElementById('chat-history-list');
+ if (sessionsListEl && sessionsListEl.textContent.includes('Loading session')) {
+ console.warn('[attachToSession] Loading stuck - forcing clear');
+ if (window.traceExecution) {
+ window.traceExecution('chat-functions', 'Loading stuck - forcing clear', { sessionId });
+ }
+ sessionsListEl.innerHTML = `
+
+
✅ Session ready
+
Send a message to start chatting
+
+ `;
+ }
+ }, 3000);
+
+ // ============================================================
+ // HYBRID APPROACH: Connect SSE instead of WebSocket subscription
+ // ============================================================
+ // With SSE, we connect to the session's event stream directly
+ // No need to "subscribe" - the connection is session-scoped by URL
+ if (window.sseClient && window.sseClient.currentSessionId !== sessionId) {
+ console.log('[attachToSession] Connecting SSE to session:', sessionId);
+ window.sseClient.connect(sessionId);
+
+ // Register SSE event handlers for this session
+ registerSSEEventHandlers(sessionId);
+ }
// Update active state in sidebar
document.querySelectorAll('.chat-session-item').forEach(item => {
@@ -360,16 +576,182 @@ function attachToSession(sessionId) {
});
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();
+ }
}
-// Subscribe to session via WebSocket
+// Refresh session list without triggering attachment
+window.refreshSessionList = async function() {
+ try {
+ const res = await fetch('/claude/api/claude/sessions');
+ if (!res.ok) return;
+
+ const data = await res.json();
+ const activeSessions = data.active || [];
+ const sessionsListEl = document.getElementById('chat-history-list');
+
+ if (!sessionsListEl) return;
+
+ // Only update if we're not showing "Loading session..."
+ if (sessionsListEl.textContent.includes('Loading session')) {
+ return;
+ }
+
+ if (activeSessions.length === 0) {
+ sessionsListEl.innerHTML = `
+
+
✅ Session ready
+
Send a message to start chatting
+
+ `;
+ return;
+ }
+
+ sessionsListEl.innerHTML = activeSessions.map(session => {
+ const projectName = session.metadata && session.metadata.project ?
+ session.metadata.project :
+ session.id.substring(0, 20);
+ const isActive = session.id === attachedSessionId ? 'active' : '';
+ return `
+
+
💬
+
+
${projectName}
+
+ ${new Date(session.createdAt).toLocaleDateString()}
+ Running
+
+
+
+ `;
+ }).join('');
+ } catch (error) {
+ console.error('[refreshSessionList] Error:', error);
+ }
+};
+
+// ============================================================
+// HYBRID APPROACH: Register SSE event handlers
+// ============================================================
+// Map SSE events to the existing WebSocket message handlers
+let SSE_HANDLERS_REGISTERED = false; // Guard flag to prevent duplicate registrations
+
+function registerSSEEventHandlers(sessionId) {
+ if (!window.sseClient) return;
+
+ // GUARD: Only register handlers once to prevent duplicate AI responses
+ if (SSE_HANDLERS_REGISTERED) {
+ console.log('[registerSSEEventHandlers] Handlers already registered, skipping duplicate registration');
+ return;
+ }
+ SSE_HANDLERS_REGISTERED = true;
+ console.log('[registerSSEEventHandlers] Registering handlers for first time');
+
+ // Session output - handle AI responses
+ window.sseClient.on('session-output', (event) => {
+ console.log('[SSE] session-output:', event);
+
+ if (window.traceExecution) {
+ window.traceExecution('chat-functions', 'SSE session-output received', {
+ sessionId: event.sessionId,
+ type: event.type,
+ contentLength: event.content?.length || 0,
+ contentPreview: event.content?.substring(0, 100) || '',
+ attachedSessionId
+ });
+ }
+
+ // Pass event directly - handleSessionOutput expects data.data.content structure
+ handleSessionOutput({
+ sessionId: event.sessionId,
+ data: {
+ type: event.type || 'stdout',
+ content: event.content,
+ timestamp: event.timestamp || Date.now()
+ }
+ });
+ });
+
+ // Session error - handle errors
+ window.sseClient.on('session-error', (event) => {
+ console.log('[SSE] session-error:', event);
+
+ if (window.traceExecution) {
+ window.traceExecution('chat-functions', 'SSE session-error received', {
+ sessionId: event.sessionId,
+ error: event.error,
+ code: event.code
+ });
+ }
+
+ // Auto-report error for fixing
+ if (window.AutoFixLogger) {
+ window.AutoFixLogger.log('SSE session error', {
+ sessionId: event.sessionId,
+ error: event.error,
+ code: event.code
+ });
+ }
+ // Pass event directly
+ handleSessionOutput({
+ sessionId: event.sessionId,
+ data: {
+ type: 'error',
+ error: event.error,
+ code: event.code,
+ timestamp: event.timestamp || Date.now()
+ }
+ });
+ });
+
+ // Approval request - handle terminal command approvals
+ window.sseClient.on('approval-request', (event) => {
+ console.log('[SSE] approval-request:', event);
+ // Trigger approval UI
+ if (typeof handleApprovalRequest === 'function') {
+ handleApprovalRequest(event);
+ }
+ });
+
+ // Approval confirmed/expired
+ window.sseClient.on('approval-confirmed', (event) => {
+ console.log('[SSE] approval-confirmed:', event);
+ });
+
+ window.sseClient.on('approval-expired', (event) => {
+ console.log('[SSE] approval-expired:', event);
+ });
+
+ console.log('[SSE] Event handlers registered for session:', sessionId);
+}
+
+// Subscribe to session via WebSocket (LEGACY - for backward compatibility)
+// This function is deprecated and will be removed once SSE is fully integrated
function subscribeToSession(sessionId) {
+ // ============================================================
+ // HYBRID APPROACH: SSE replaces WebSocket subscription
+ // ============================================================
+ // SSE connections are session-scoped by URL, so no explicit
+ // subscription is needed. The SSE client handles this automatically.
+ if (window.sseClient && window.sseClient.currentSessionId !== sessionId) {
+ console.log('[subscribeToSession] Connecting SSE (replaces WebSocket subscription):', sessionId);
+ window.sseClient.connect(sessionId);
+ registerSSEEventHandlers(sessionId);
+ return;
+ }
+
+ // Fallback to WebSocket if SSE not available
if (window.ws && window.ws.readyState === WebSocket.OPEN) {
window.ws.send(JSON.stringify({
type: 'subscribe',
sessionId: sessionId
}));
- console.log('Subscribed to session:', 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...');
@@ -394,16 +776,64 @@ function subscribeToSession(sessionId) {
}
// 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();
- // Add existing messages from output buffer - restore both user and assistant messages
- data.session.outputBuffer.forEach(entry => {
+ const messages = data.session.outputBuffer;
+ const BATCH_SIZE = 20;
+ const totalMessages = messages.length;
+
+ if (totalMessages === 0) {
+ // New session with no messages yet - clear loading state and show ready state
+ if (window.traceExecution) {
+ window.traceExecution('chat-functions', 'loadSessionMessages - empty session, showing chat interface', { sessionId });
+ }
+ // Clear the "Loading..." message from the sidebar and refresh to show all sessions
+ const sessionsListEl = document.getElementById('chat-history-list');
+ if (sessionsListEl) {
+ sessionsListEl.innerHTML = `
+
+
✅ Session ready
+
Send a message to start chatting
+
+ `;
+ }
+ // Refresh sidebar to show all active sessions
+ if (typeof loadChatHistory === 'function') {
+ setTimeout(() => loadChatHistory(), 100);
+ } else if (typeof window.refreshSessionList === 'function') {
+ setTimeout(() => window.refreshSessionList(), 100);
+ }
+ return;
+ }
+
+ console.log(`[loadSessionMessages] Loading ${totalMessages} messages in batches of ${BATCH_SIZE}`);
+
+ // Process first batch immediately
+ const firstBatch = messages.slice(0, BATCH_SIZE);
+ for (const entry of firstBatch) {
// Check if entry has role information (newer format)
if (entry.role) {
appendMessage(entry.role, entry.content, false);
@@ -411,10 +841,58 @@ async function loadSessionMessages(sessionId) {
// 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);
}
}
@@ -524,21 +1002,48 @@ function dismissModeSuggestion() {
}
// Send Chat Message (Enhanced with smart input parsing)
-async function sendChatMessage() {
+// @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 = input.value.trim();
+ 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) {
+ if (window.semanticValidator && !options.skipValidation) {
// Track user message for context
window.semanticValidator.trackUserMessage(message);
// Get the mode BEFORE any validation
- const selectedMode = currentChatMode || 'auto';
+ 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!
@@ -707,12 +1212,17 @@ The AI assistant asked for your approval, but you responded in
Terminal
}
}
- // Use selected mode from buttons, or fall back to parsed mode
- const selectedMode = currentChatMode || 'auto';
+ // Use selected mode from buttons, or fall back to parsed mode, or override
+ const selectedMode = modeOverride || currentChatMode || 'auto';
- // Add user message to chat
- appendMessage('user', message);
- clearInput();
+ // 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();
@@ -724,80 +1234,106 @@ The AI assistant asked for your approval, but you responded in Terminal
return;
}
+ // ============================================================
+ // HYBRID APPROACH: Send commands via REST API instead of WebSocket
+ // ============================================================
+ // SSE is for receiving events only. Commands are sent via REST API.
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;
- }
+ console.log('[sendChatMessage] Sending command via REST API to session:', attachedSessionId);
- 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
+ // Prepare request body
+ const requestBody = {
+ command: message,
+ mode: selectedMode
};
- // Add metadata if available (files, commands, mode)
+ // Add metadata if available (files, commands)
const hasFiles = parsed && parsed.files.length > 0;
const hasCommands = parsed && parsed.commands.length > 0;
- const modeNotAuto = selectedMode !== 'auto';
- if (hasFiles || hasCommands || modeNotAuto) {
- payload.metadata = {
+ if (hasFiles || hasCommands) {
+ requestBody.metadata = {
files: parsed ? parsed.files : [],
commands: parsed ? parsed.commands : [],
- mode: selectedMode,
hasFileReferences: parsed ? parsed.hasFileReferences : false
};
- console.log('Sending with metadata:', payload.metadata);
+ console.log('[sendChatMessage] Sending with metadata:', requestBody.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
+ // 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)
});
- // 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('[sendChatMessage] Response status:', response.status);
+
+ if (window.traceExecution) {
+ window.traceExecution('chat-functions', 'sendChatMessage - API response', {
+ status: response.status,
+ ok: response.ok
+ });
}
- console.log('Sent command via WebSocket:', message.substring(0, 50));
+
+ 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('Error sending message:', error);
+ console.error('[sendChatMessage] Error sending message:', error);
hideStreamingIndicator();
setGeneratingState(false);
appendSystemMessage('Failed to send message: ' + error.message);
@@ -1098,6 +1634,16 @@ async function executeNativeCommand(message, sessionId) {
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) {
@@ -1351,4 +1897,28 @@ if (typeof window !== 'undefined') {
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);
+ }
}
diff --git a/public/claude-ide/index.html b/public/claude-ide/index.html
index 94fe1ba9..849cc19e 100644
--- a/public/claude-ide/index.html
+++ b/public/claude-ide/index.html
@@ -4,16 +4,276 @@
Claude Code IDE
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -345,20 +605,24 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+