- 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>
1664 lines
62 KiB
JavaScript
1664 lines
62 KiB
JavaScript
// ============================================
|
||
// Chat Interface Functions - Improved Session Resumption
|
||
// ============================================
|
||
|
||
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
|
||
|
||
// ============================================
|
||
// IMPROVEMENT: Session State Detection
|
||
// ============================================
|
||
/**
|
||
* Get session state from API
|
||
* @param {string} sessionId
|
||
* @returns {Promise<Object|null>}
|
||
*/
|
||
async function getSessionState(sessionId) {
|
||
try {
|
||
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`);
|
||
if (!res.ok) {
|
||
if (res.status === 404) {
|
||
return { exists: false, error: 'Session not found' };
|
||
}
|
||
throw new Error(`HTTP ${res.status}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
if (!data.session) {
|
||
return { exists: false, error: 'No session data' };
|
||
}
|
||
|
||
return {
|
||
exists: true,
|
||
status: data.session.status,
|
||
isRunning: data.session.status === 'running',
|
||
isStopped: data.session.status === 'stopped',
|
||
isTerminated: data.session.status === 'terminated',
|
||
messageCount: data.session.outputBuffer?.length || 0,
|
||
workingDir: data.session.workingDir,
|
||
createdAt: data.session.createdAt,
|
||
lastActivity: data.session.lastActivity,
|
||
session: data.session
|
||
};
|
||
} catch (error) {
|
||
console.error('[getSessionState] Error:', error);
|
||
return { exists: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// IMPROVEMENT: Enhanced attachToSession with validation
|
||
// ============================================
|
||
/**
|
||
* Attach to an existing session
|
||
* Validates session state and handles historical/stopped sessions appropriately
|
||
* @param {string} sessionId
|
||
*/
|
||
async function attachToSession(sessionId) {
|
||
console.log('[attachToSession] Attaching to session:', sessionId);
|
||
|
||
try {
|
||
// Show loading indicator
|
||
if (typeof appendSystemMessage === 'function') {
|
||
appendSystemMessage('🔄 Loading session...');
|
||
}
|
||
|
||
// ============================================
|
||
// CRITICAL FIX: Validate session exists first
|
||
// ============================================
|
||
const sessionState = await getSessionState(sessionId);
|
||
|
||
if (!sessionState.exists) {
|
||
const errorMsg = sessionState.error || 'Session not found';
|
||
console.error('[attachToSession] Session validation failed:', errorMsg);
|
||
|
||
if (typeof appendSystemMessage === 'function') {
|
||
appendSystemMessage(`❌ Failed to load session: ${errorMsg}`);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const session = sessionState.session;
|
||
|
||
// Set session IDs
|
||
attachedSessionId = sessionId;
|
||
chatSessionId = sessionId;
|
||
|
||
// Update UI
|
||
const sessionIdEl = document.getElementById('current-session-id');
|
||
if (sessionIdEl) {
|
||
sessionIdEl.textContent = sessionId;
|
||
}
|
||
|
||
// Update context panel with appropriate status
|
||
if (typeof contextPanel !== 'undefined' && contextPanel) {
|
||
let status = 'active';
|
||
if (sessionState.isStopped || sessionState.isTerminated) {
|
||
status = 'historical';
|
||
}
|
||
contextPanel.setSession(sessionId, status);
|
||
}
|
||
|
||
// Clear chat display first
|
||
if (typeof clearChatDisplay === 'function') {
|
||
clearChatDisplay();
|
||
}
|
||
|
||
// Load historical messages
|
||
await loadSessionMessages(sessionId);
|
||
|
||
// ============================================
|
||
// CRITICAL FIX: Subscribe only if session is running
|
||
// ============================================
|
||
if (sessionState.isRunning) {
|
||
const subscribed = await subscribeToSession(sessionId);
|
||
if (subscribed) {
|
||
if (typeof appendSystemMessage === 'function') {
|
||
appendSystemMessage(`✅ Attached to active session: ${sessionId.substring(0, 12)}...`);
|
||
}
|
||
} else {
|
||
if (typeof appendSystemMessage === 'function') {
|
||
appendSystemMessage(`⚠️ Session loaded but real-time updates unavailable`);
|
||
}
|
||
}
|
||
} else {
|
||
// Historical/stopped session - show appropriate message
|
||
const statusText = sessionState.isTerminated ? 'terminated' : 'stopped';
|
||
const createdDate = new Date(session.createdAt).toLocaleString();
|
||
|
||
if (typeof appendSystemMessage === 'function') {
|
||
appendSystemMessage(`📂 Viewing ${statusText} session from ${createdDate}`);
|
||
appendSystemMessage('ℹ️ This session is read-only. Start a new chat to continue working.');
|
||
|
||
// Offer to create new session with same working directory
|
||
setTimeout(() => {
|
||
if (typeof appendSystemMessage === 'function') {
|
||
const workingDir = session.workingDir || 'unknown';
|
||
appendSystemMessage(`💡 Working directory: ${workingDir}`);
|
||
}
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
// Update sidebar active state
|
||
updateSessionSidebarActiveState(sessionId);
|
||
|
||
} catch (error) {
|
||
console.error('[attachToSession] Error:', error);
|
||
if (typeof appendSystemMessage === 'function') {
|
||
appendSystemMessage(`❌ Failed to attach to session: ${error.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Update the sidebar to show the active session
|
||
*/
|
||
function updateSessionSidebarActiveState(sessionId) {
|
||
document.querySelectorAll('.chat-session-item, .chat-history-item').forEach(item => {
|
||
item.classList.remove('active');
|
||
const onclick = item.getAttribute('onclick');
|
||
if (onclick && onclick.includes(sessionId)) {
|
||
item.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// IMPROVEMENT: Robust subscribeToSession with retries
|
||
// ============================================
|
||
/**
|
||
* Subscribe to session via WebSocket
|
||
* Implements retry logic and proper connection waiting
|
||
* @param {string} sessionId
|
||
* @param {number} retries - Number of retry attempts
|
||
* @returns {Promise<boolean>} - Success status
|
||
*/
|
||
async function subscribeToSession(sessionId, retries = 3) {
|
||
for (let attempt = 0; attempt < retries; attempt++) {
|
||
try {
|
||
console.log(`[subscribeToSession] Attempt ${attempt + 1}/${retries} for session: ${sessionId}`);
|
||
|
||
// Wait for WebSocket to be ready
|
||
if (typeof waitForWebSocketReady === 'function') {
|
||
await waitForWebSocketReady(3000);
|
||
} else {
|
||
// Fallback: simple polling
|
||
const maxWait = 3000;
|
||
const start = Date.now();
|
||
while ((!window.wsReady || window.ws?.readyState !== WebSocket.OPEN) && Date.now() - start < maxWait) {
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
}
|
||
}
|
||
|
||
// Check if ready
|
||
if (!window.wsReady || window.ws?.readyState !== WebSocket.OPEN) {
|
||
throw new Error('WebSocket not ready after timeout');
|
||
}
|
||
|
||
// Send subscription message
|
||
window.ws.send(JSON.stringify({
|
||
type: 'subscribe',
|
||
sessionId: sessionId
|
||
}));
|
||
|
||
console.log('✅ [subscribeToSession] Successfully subscribed to:', sessionId);
|
||
return true;
|
||
|
||
} catch (error) {
|
||
console.warn(`[subscribeToSession] Attempt ${attempt + 1} failed:`, error.message);
|
||
|
||
if (attempt === retries - 1) {
|
||
console.error('[subscribeToSession] All retries exhausted');
|
||
return false;
|
||
}
|
||
|
||
// Exponential backoff before retry
|
||
const backoffDelay = Math.pow(2, attempt) * 1000;
|
||
console.log(`[subscribeToSession] Waiting ${backoffDelay}ms before retry...`);
|
||
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Reset all chat state
|
||
function resetChatState() {
|
||
console.log('Resetting chat state...');
|
||
chatSessionId = null;
|
||
chatMessages = [];
|
||
attachedSessionId = null;
|
||
console.log('Chat state reset complete');
|
||
}
|
||
|
||
// Load Chat View
|
||
async function loadChatView() {
|
||
console.log('[loadChatView] Loading chat view...');
|
||
|
||
// Check if there's a pending session from Sessions view
|
||
if (window.pendingSessionId) {
|
||
console.log('[loadChatView] Detected pending session:', window.pendingSessionId);
|
||
|
||
const sessionId = window.pendingSessionId;
|
||
const sessionData = window.pendingSessionData;
|
||
|
||
// Clear pending session (consume it)
|
||
window.pendingSessionId = null;
|
||
window.pendingSessionData = null;
|
||
|
||
// Load the session
|
||
await loadSessionIntoChat(sessionId, sessionData);
|
||
return;
|
||
}
|
||
|
||
// Preserve attached session ID if it exists (for auto-session workflow)
|
||
const preservedSessionId = attachedSessionId;
|
||
|
||
// Reset state on view load to prevent stale session references
|
||
resetChatState();
|
||
|
||
// Restore attached session if it was set (e.g., from auto-session initialization)
|
||
if (preservedSessionId) {
|
||
console.log('[loadChatView] Restoring attached session:', preservedSessionId);
|
||
attachedSessionId = preservedSessionId;
|
||
chatSessionId = preservedSessionId;
|
||
}
|
||
|
||
// Load chat sessions
|
||
try {
|
||
console.log('[loadChatView] Fetching sessions...');
|
||
const res = await fetch('/claude/api/claude/sessions');
|
||
|
||
if (!res.ok) {
|
||
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
console.log('[loadChatView] Raw sessions data:', {
|
||
activeCount: (data.active || []).length,
|
||
historicalCount: (data.historical || []).length,
|
||
activeIds: (data.active || []).map(s => ({ id: s.id, status: s.status }))
|
||
});
|
||
|
||
const sessionsListEl = document.getElementById('chat-history-list');
|
||
|
||
if (!sessionsListEl) {
|
||
console.error('[loadChatView] chat-history-list element not found!');
|
||
return;
|
||
}
|
||
|
||
// ONLY show active sessions - no historical sessions in chat view
|
||
// Historical sessions are read-only and can't receive new messages
|
||
let activeSessions = (data.active || []).filter(s => s.status === 'running');
|
||
console.log('[loadChatView] Running sessions after status filter:', activeSessions.length);
|
||
|
||
// Filter by current project if in project context
|
||
const currentProjectDir = window.currentProjectDir;
|
||
|
||
if (currentProjectDir) {
|
||
console.log('[loadChatView] Current project dir:', currentProjectDir);
|
||
|
||
// Filter sessions that belong to this project
|
||
activeSessions = activeSessions.filter(session => {
|
||
// Check if session's working directory is within current project directory
|
||
const sessionWorkingDir = session.workingDir || '';
|
||
|
||
// Direct match: session working dir starts with project dir
|
||
const directMatch = sessionWorkingDir.startsWith(currentProjectDir);
|
||
|
||
// Metadata match: session metadata project matches
|
||
const metadataMatch = session.metadata?.project === currentProjectDir;
|
||
|
||
// For project sessions, also check if project path is in working dir
|
||
const pathMatch = sessionWorkingDir.includes(currentProjectDir) || currentProjectDir.includes(sessionWorkingDir);
|
||
|
||
const isMatch = directMatch || metadataMatch || pathMatch;
|
||
|
||
console.log(`[loadChatView] Session ${session.id}:`, {
|
||
workingDir: sessionWorkingDir,
|
||
projectDir: currentProjectDir,
|
||
directMatch,
|
||
metadataMatch,
|
||
pathMatch,
|
||
isMatch
|
||
});
|
||
|
||
return isMatch;
|
||
});
|
||
|
||
console.log('[loadChatView] Project sessions found:', activeSessions.length, 'out of', (data.active || []).length);
|
||
}
|
||
|
||
console.log('Active sessions (can receive messages):', activeSessions.length);
|
||
|
||
if (activeSessions.length > 0) {
|
||
sessionsListEl.innerHTML = activeSessions.map(session => {
|
||
const projectName = session.metadata && session.metadata.project ?
|
||
session.metadata.project :
|
||
session.id.substring(0, 20);
|
||
return `
|
||
<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 {
|
||
// Zero-friction entry: Auto-create session in project context
|
||
if (currentProjectName && window.currentProjectDir) {
|
||
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
|
||
}
|
||
});
|
||
|
||
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
|
||
}
|
||
})
|
||
});
|
||
|
||
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';
|
||
|
||
// 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));
|
||
await loadChatView().catch(err => console.error('[startNewChat] Background refresh failed:', err));
|
||
|
||
// Hide the creation success message after a short delay
|
||
setTimeout(() => {
|
||
const loadingMsg = document.querySelector('.chat-system');
|
||
if (loadingMsg && loadingMsg.textContent.includes('Creating new chat session')) {
|
||
loadingMsg.remove();
|
||
}
|
||
}, 2000);
|
||
|
||
// Focus on input
|
||
const input = document.getElementById('chat-input');
|
||
if (input) {
|
||
input.focus();
|
||
}
|
||
} else {
|
||
throw new Error('Session created but no ID returned');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating new chat session:', error);
|
||
appendSystemMessage('❌ Failed to create new chat session: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Load Session Messages
|
||
// Uses batching to prevent UI blocking with large message buffers
|
||
async function loadSessionMessages(sessionId) {
|
||
try {
|
||
const res = await fetch('/claude/api/claude/sessions/' + sessionId);
|
||
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) {
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading session messages:', error);
|
||
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;
|
||
|
||
// ============================================================
|
||
// 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;
|
||
}
|
||
|
||
try {
|
||
// Check WebSocket state
|
||
if (!window.ws) {
|
||
console.error('WebSocket is null/undefined');
|
||
appendSystemMessage('WebSocket not initialized. Please refresh the page.');
|
||
hideStreamingIndicator();
|
||
setGeneratingState(false);
|
||
return;
|
||
}
|
||
|
||
const state = window.ws.readyState;
|
||
const stateName = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][state] || 'UNKNOWN';
|
||
|
||
console.log('WebSocket state:', state, stateName);
|
||
|
||
if (state !== WebSocket.OPEN) {
|
||
console.error('WebSocket not in OPEN state:', stateName);
|
||
appendSystemMessage(`WebSocket not ready (state: ${stateName}). Retrying...`);
|
||
hideStreamingIndicator();
|
||
setGeneratingState(false);
|
||
|
||
// Trigger reconnection if closed
|
||
if (state === WebSocket.CLOSED) {
|
||
console.log('WebSocket closed, triggering reconnection...');
|
||
if (typeof connectWebSocket === 'function') {
|
||
connectWebSocket();
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Send command via WebSocket with parsed metadata
|
||
const payload = {
|
||
type: 'command',
|
||
sessionId: attachedSessionId,
|
||
command: message
|
||
};
|
||
|
||
// Add metadata if available (files, commands, mode)
|
||
const hasFiles = parsed && parsed.files.length > 0;
|
||
const hasCommands = parsed && parsed.commands.length > 0;
|
||
const modeNotAuto = selectedMode !== 'auto';
|
||
|
||
if (hasFiles || hasCommands || modeNotAuto) {
|
||
payload.metadata = {
|
||
files: parsed ? parsed.files : [],
|
||
commands: parsed ? parsed.commands : [],
|
||
mode: selectedMode,
|
||
hasFileReferences: parsed ? parsed.hasFileReferences : false
|
||
};
|
||
console.log('Sending with metadata:', payload.metadata);
|
||
}
|
||
|
||
// Debug logging before sending
|
||
console.log('[DEBUG] About to send command payload:', {
|
||
type: payload.type,
|
||
sessionId: payload.sessionId,
|
||
commandLength: payload.command?.length,
|
||
wsReady: window.wsReady,
|
||
wsState: window.ws?.readyState,
|
||
queueLength: window.messageQueue?.length || 0
|
||
});
|
||
|
||
// Use message queue to prevent race conditions
|
||
if (typeof queueMessage === 'function') {
|
||
queueMessage(payload);
|
||
console.log('[DEBUG] Message queued, queue length now:', window.messageQueue?.length);
|
||
} else {
|
||
window.ws.send(JSON.stringify(payload));
|
||
console.log('[DEBUG] Sent directly via WebSocket (no queue function)');
|
||
}
|
||
console.log('Sent command via WebSocket:', message.substring(0, 50));
|
||
} catch (error) {
|
||
console.error('Error sending message:', error);
|
||
hideStreamingIndicator();
|
||
setGeneratingState(false);
|
||
appendSystemMessage('Failed to send message: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Task 1: Set Generating State (show/hide stop button)
|
||
function setGeneratingState(generating) {
|
||
isGenerating = generating;
|
||
|
||
const sendButton = document.getElementById('send-button');
|
||
const stopButton = document.getElementById('stop-button');
|
||
|
||
if (generating) {
|
||
// Show stop button, hide send button
|
||
if (sendButton) sendButton.classList.add('hidden');
|
||
if (stopButton) stopButton.classList.remove('hidden');
|
||
} else {
|
||
// Show send button, hide stop button
|
||
if (sendButton) sendButton.classList.remove('hidden');
|
||
if (stopButton) stopButton.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// Task 1: Stop Generation
|
||
function stopGeneration() {
|
||
console.log('Stopping generation...');
|
||
|
||
if (!window.ws || window.ws.readyState !== WebSocket.OPEN) {
|
||
appendSystemMessage('⚠️ Cannot stop: WebSocket not connected');
|
||
return;
|
||
}
|
||
|
||
// Send stop signal via WebSocket
|
||
window.ws.send(JSON.stringify({
|
||
type: 'stop',
|
||
sessionId: attachedSessionId
|
||
}));
|
||
|
||
appendSystemMessage('⏸️ Stopping generation...');
|
||
|
||
// Update UI state
|
||
hideStreamingIndicator();
|
||
setGeneratingState(false);
|
||
}
|
||
|
||
// Helper function to get file icon for context panel
|
||
function getFileIcon(ext) {
|
||
const icons = {
|
||
'js': '📜', 'mjs': '📜', 'ts': '📘', 'tsx': '⚛️', 'jsx': '⚛️',
|
||
'html': '🌐', 'htm': '🌐', 'css': '🎨', 'scss': '🎨',
|
||
'py': '🐍', 'rb': '💎', 'php': '🐘',
|
||
'json': '📋', 'xml': '📄',
|
||
'md': '📝', 'txt': '📄',
|
||
'png': '🖼️', 'jpg': '🖼️', 'jpeg': '🖼️', 'gif': '🖼️', 'svg': '🖼️',
|
||
'sh': '🖥️', 'bash': '🖥️', 'zsh': '🖥️',
|
||
'yml': '⚙️', 'yaml': '⚙️', 'toml': '⚙️'
|
||
};
|
||
return icons[ext] || '📄';
|
||
}
|
||
|
||
// Chat Mode Management
|
||
let currentChatMode = 'auto';
|
||
|
||
function setChatMode(mode) {
|
||
currentChatMode = mode;
|
||
|
||
// Update button states
|
||
document.querySelectorAll('.mode-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
if (btn.dataset.mode === mode) {
|
||
btn.classList.add('active');
|
||
}
|
||
});
|
||
|
||
// Update context panel with mode
|
||
if (typeof contextPanel !== 'undefined' && contextPanel) {
|
||
contextPanel.setMode(mode);
|
||
}
|
||
|
||
// Initialize WebContainer if switching to webcontainer mode
|
||
if (mode === 'webcontainer') {
|
||
initializeWebContainer();
|
||
}
|
||
|
||
// Show mode change message
|
||
const modeNames = {
|
||
'auto': '🤖 Auto (best mode will be detected automatically)',
|
||
'native': '💻 Native (commands execute directly on your system)',
|
||
'webcontainer': '💻 Terminal (executes commands in persistent terminal session)'
|
||
};
|
||
|
||
appendSystemMessage(`Execution mode changed to: ${modeNames[mode]}`);
|
||
console.log('Chat mode set to:', mode);
|
||
}
|
||
|
||
// Initialize WebContainer for current session
|
||
async function initializeWebContainer() {
|
||
try {
|
||
if (typeof webContainerManager === 'undefined') {
|
||
appendSystemMessage('⚠️ WebContainer Manager not loaded. Refresh the page.');
|
||
return;
|
||
}
|
||
|
||
// Check if already initialized for current session
|
||
const sessionId = attachedSessionId || chatSessionId;
|
||
if (!sessionId) {
|
||
appendSystemMessage('⚠️ Please start a chat session first.');
|
||
return;
|
||
}
|
||
|
||
const status = webContainerManager.getStatus();
|
||
if (status.initialized && status.currentSession === sessionId) {
|
||
appendSystemMessage('✅ WebContainer already initialized for this session');
|
||
return;
|
||
}
|
||
|
||
appendSystemMessage('🔄 Initializing WebContainer environment...');
|
||
|
||
await webContainerManager.initialize(sessionId);
|
||
|
||
appendSystemMessage('✅ WebContainer ready! Commands will execute in browser sandbox.');
|
||
} catch (error) {
|
||
console.error('Failed to initialize WebContainer:', error);
|
||
appendSystemMessage('❌ Failed to initialize WebContainer: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Detect if message is a shell command (using semantic validator)
|
||
function isShellCommand(message) {
|
||
if (window.semanticValidator) {
|
||
return window.semanticValidator.isShellCommand(message);
|
||
}
|
||
|
||
// Fallback to basic detection if validator not loaded
|
||
console.warn('[SemanticValidator] Not loaded, using basic command detection');
|
||
const trimmed = message.trim().toLowerCase();
|
||
|
||
// Basic conversational check
|
||
const conversationalPatterns = [
|
||
/^(if|when|where|what|how|why|who|which|whose|can|could|would|should|will|do|does|did|is|are|was|were|am|have|has|had|please|thank|hey|hello|hi)\s/i,
|
||
/^(i|you|he|she|it|we|they)\s/i,
|
||
/^(yes|no|maybe|ok|okay|sure|alright)\s/i
|
||
];
|
||
|
||
if (conversationalPatterns.some(pattern => pattern.test(trimmed))) {
|
||
return false;
|
||
}
|
||
|
||
// Basic command patterns
|
||
const basicPatterns = [
|
||
/^(cd|ls|pwd|echo|cat|grep|find|rm|cp|mv|mkdir|npm|git|ping|curl|wget|node|python)(\s|$)/,
|
||
/^\//,
|
||
/^[a-z0-9_\-./]+\s*[\|>]/
|
||
];
|
||
|
||
return basicPatterns.some(pattern => pattern.test(trimmed));
|
||
}
|
||
|
||
// Send shell command to active Claude CLI session via WebSocket
|
||
async function sendShellCommand(sessionId, command) {
|
||
try {
|
||
// Use WebSocket to send shell command (backend will execute through native mode)
|
||
if (!window.ws || window.ws.readyState !== WebSocket.OPEN) {
|
||
throw new Error('WebSocket not connected');
|
||
}
|
||
|
||
// Send command via WebSocket
|
||
window.ws.send(JSON.stringify({
|
||
type: 'command',
|
||
sessionId: sessionId,
|
||
command: command,
|
||
metadata: {
|
||
executionMode: 'native',
|
||
timestamp: new Date().toISOString()
|
||
}
|
||
}));
|
||
|
||
// Return a promise that will be resolved when the command completes
|
||
// Note: The actual output will come through WebSocket messages
|
||
return {
|
||
success: true,
|
||
message: 'Command sent via WebSocket'
|
||
};
|
||
} catch (error) {
|
||
console.error('[Shell Command] Error:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Handle command execution in Full Stack mode (via Claude CLI session's stdin)
|
||
async function handleWebContainerCommand(message) {
|
||
const sessionId = attachedSessionId || chatSessionId;
|
||
if (!sessionId) {
|
||
appendSystemMessage('⚠️ No active session.');
|
||
hideStreamingIndicator();
|
||
setGeneratingState(false);
|
||
return;
|
||
}
|
||
|
||
// Smart command detection
|
||
if (!isShellCommand(message)) {
|
||
hideStreamingIndicator();
|
||
setGeneratingState(false);
|
||
appendSystemMessage(`💬 This looks like a conversational message, not a shell command.
|
||
|
||
Terminal mode executes commands directly. For example:
|
||
• <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');
|
||
|
||
// 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; }
|
||
});
|
||
}
|
||
|
||
console.log('=== IMPROVED CHAT FUNCTIONS LOADED: Session Resumption Fixed ===');
|