- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1999 lines
78 KiB
JavaScript
1999 lines
78 KiB
JavaScript
// ============================================
|
||
// 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...');
|
||
console.log('[startNewChat] Request payload:', {
|
||
workingDir,
|
||
metadata: {
|
||
type: 'chat',
|
||
source: 'web-ide',
|
||
project: projectName,
|
||
projectPath: window.currentProjectDir || null
|
||
}
|
||
});
|
||
|
||
// CRITICAL FIX: Add timeout to prevent hanging
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||
|
||
const res = await fetch('/claude/api/claude/sessions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
workingDir: workingDir,
|
||
metadata: {
|
||
type: 'chat',
|
||
source: 'web-ide',
|
||
project: projectName,
|
||
projectPath: window.currentProjectDir || null
|
||
}
|
||
}),
|
||
signal: controller.signal
|
||
});
|
||
|
||
clearTimeout(timeoutId); // Clear timeout if request completes
|
||
|
||
console.log('[startNewChat] Response status:', res.status, res.statusText);
|
||
|
||
if (!res.ok) {
|
||
const errorText = await res.text();
|
||
console.error('[startNewChat] API error response:', errorText);
|
||
throw new Error(`HTTP ${res.status}: ${res.statusText} - ${errorText}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
console.log('Session creation response:', data);
|
||
|
||
if (!data || (!data.success && !data.id)) {
|
||
console.error('[startNewChat] Invalid response:', data);
|
||
throw new Error(data?.error || data?.message || 'Invalid response from server');
|
||
}
|
||
|
||
// Handle both {success: true, session: {...}} and direct session object responses
|
||
const session = data.session || data;
|
||
|
||
if (session.id) {
|
||
attachedSessionId = session.id;
|
||
chatSessionId = session.id;
|
||
|
||
console.log('New session created:', session.id);
|
||
|
||
// Update UI
|
||
document.getElementById('current-session-id').textContent = session.id;
|
||
document.getElementById('chat-title').textContent = projectName ? `Project: ${projectName}` : 'New Chat';
|
||
|
||
// CRITICAL FIX: Add new session to tabs and set as active
|
||
if (window.sessionTabs) {
|
||
// Add session to tabs
|
||
if (typeof window.sessionTabs.updateSession === 'function') {
|
||
window.sessionTabs.updateSession(session);
|
||
console.log('[startNewChat] Added new session to tabs:', session.id);
|
||
}
|
||
// Set as active
|
||
if (typeof window.sessionTabs.setActiveSession === 'function') {
|
||
window.sessionTabs.setActiveSession(session.id);
|
||
console.log('[startNewChat] Set new session as active in tabs:', session.id);
|
||
}
|
||
}
|
||
|
||
// Subscribe to session via WebSocket
|
||
subscribeToSession(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));
|
||
}
|
||
|
||
// CRITICAL FIX: Immediately remove the "Creating new chat session..." message
|
||
const messagesContainer = document.getElementById('chat-messages');
|
||
if (messagesContainer) {
|
||
const loadingMsgs = messagesContainer.querySelectorAll('.chat-system');
|
||
loadingMsgs.forEach(msg => {
|
||
if (msg.textContent.includes('Creating new chat session')) {
|
||
msg.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Focus on input
|
||
const input = document.getElementById('chat-input');
|
||
if (input) {
|
||
input.focus();
|
||
}
|
||
} else {
|
||
throw new Error('Session created but no ID returned');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating new chat session:', error);
|
||
|
||
// CRITICAL FIX: Remove the "Creating new chat session..." message before showing error
|
||
const messagesContainer = document.getElementById('chat-messages');
|
||
if (messagesContainer) {
|
||
const loadingMsgs = messagesContainer.querySelectorAll('.chat-system');
|
||
loadingMsgs.forEach(msg => {
|
||
if (msg.textContent.includes('Creating new chat session')) {
|
||
msg.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Special handling for timeout/abort errors
|
||
if (error.name === 'AbortError') {
|
||
appendSystemMessage('❌ Request timed out. The server took too long to respond. Please try again.');
|
||
} else {
|
||
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;
|
||
|
||
// CRITICAL FIX: Update session tabs to mark this session as active
|
||
if (window.sessionTabs && typeof window.sessionTabs.setActiveSession === 'function') {
|
||
window.sessionTabs.setActiveSession(sessionId);
|
||
console.log('[attachToSession] Updated session tabs active session to:', 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);
|
||
}
|
||
}
|