- Real-time error monitoring system with WebSocket - Auto-fix agent that triggers on browser errors - Bug tracker dashboard with floating button (🐛) - Live activity stream showing AI thought process - Fixed 4 JavaScript errors (SyntaxError, TypeError) - Fixed SessionPicker API endpoint error - Enhanced chat input with Monaco editor - Session picker component for project management Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1214 lines
43 KiB
JavaScript
1214 lines
43 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...');
|
||
|
||
// 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
|
||
*/
|
||
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)
|
||
if (sessionData.outputBuffer && sessionData.outputBuffer.length > 0) {
|
||
sessionData.outputBuffer.forEach(entry => {
|
||
if (entry.role) {
|
||
appendMessage(entry.role, entry.content, false);
|
||
} else {
|
||
// Legacy format - default to assistant
|
||
appendMessage('assistant', entry.content, false);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Show success message
|
||
const isRunning = sessionData.status === 'running';
|
||
const statusText = isRunning ? 'Active session' : 'Historical session';
|
||
appendSystemMessage(`✅ Loaded ${statusText} from ${new Date(sessionData.createdAt).toLocaleString()}`);
|
||
|
||
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(sessionId);
|
||
}
|
||
|
||
// Focus input for active sessions
|
||
if (isRunning) {
|
||
setTimeout(() => {
|
||
const input = document.getElementById('chat-input');
|
||
if (input) input.focus();
|
||
}, 100);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('[loadSessionIntoChat] Error:', error);
|
||
appendSystemMessage('❌ Failed to load session: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Start New Chat
|
||
async function startNewChat() {
|
||
// Reset all state first
|
||
resetChatState();
|
||
|
||
// Clear current chat
|
||
clearChatDisplay();
|
||
|
||
appendSystemMessage('Creating new chat session...');
|
||
|
||
// Determine working directory based on context
|
||
let workingDir = '/home/uroma/obsidian-vault'; // default
|
||
let projectName = null;
|
||
|
||
// If we're in a project context, use the project directory
|
||
if (window.currentProjectDir) {
|
||
workingDir = window.currentProjectDir;
|
||
projectName = window.currentProjectDir.split('/').pop();
|
||
console.log('[startNewChat] Creating session for project:', projectName, 'at', workingDir);
|
||
}
|
||
|
||
// Create new session
|
||
try {
|
||
console.log('Creating new Claude Code session...');
|
||
const res = await fetch('/claude/api/claude/sessions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
workingDir: workingDir,
|
||
metadata: {
|
||
type: 'chat',
|
||
source: 'web-ide',
|
||
project: projectName,
|
||
projectPath: window.currentProjectDir || null
|
||
}
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
console.log('Session creation response:', data);
|
||
|
||
if (data.success) {
|
||
attachedSessionId = data.session.id;
|
||
chatSessionId = data.session.id;
|
||
|
||
console.log('New session created:', data.session.id);
|
||
|
||
// Update UI
|
||
document.getElementById('current-session-id').textContent = data.session.id;
|
||
document.getElementById('chat-title').textContent = projectName ? `Project: ${projectName}` : 'New Chat';
|
||
|
||
// Subscribe to session via WebSocket
|
||
subscribeToSession(data.session.id);
|
||
|
||
// Give backend time to persist session, then refresh sidebar
|
||
// This ensures the new session appears in the list
|
||
await new Promise(resolve => setTimeout(resolve, 150));
|
||
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(data.error || 'Failed to create session');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating new chat session:', error);
|
||
appendSystemMessage('❌ Failed to create new chat session: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Attach to Existing Session
|
||
function attachToSession(sessionId) {
|
||
attachedSessionId = sessionId;
|
||
chatSessionId = sessionId;
|
||
|
||
// Update UI
|
||
document.getElementById('current-session-id').textContent = sessionId;
|
||
|
||
// Update context panel with session
|
||
if (typeof contextPanel !== 'undefined' && contextPanel) {
|
||
contextPanel.setSession(sessionId, 'active');
|
||
}
|
||
|
||
// Load session messages
|
||
loadSessionMessages(sessionId);
|
||
|
||
// Subscribe to session via WebSocket
|
||
subscribeToSession(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);
|
||
}
|
||
|
||
// Subscribe to session via WebSocket
|
||
function subscribeToSession(sessionId) {
|
||
if (window.ws && window.ws.readyState === WebSocket.OPEN) {
|
||
window.ws.send(JSON.stringify({
|
||
type: 'subscribe',
|
||
sessionId: sessionId
|
||
}));
|
||
console.log('Subscribed to session:', 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
|
||
async function loadSessionMessages(sessionId) {
|
||
try {
|
||
const res = await fetch('/claude/api/claude/sessions/' + sessionId);
|
||
const data = await res.json();
|
||
|
||
if (data.session) {
|
||
clearChatDisplay();
|
||
|
||
// Add existing messages from output buffer - restore both user and assistant messages
|
||
data.session.outputBuffer.forEach(entry => {
|
||
// 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);
|
||
}
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading session messages:', error);
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
async function sendChatMessage() {
|
||
const input = document.getElementById('chat-input');
|
||
const message = input.value.trim();
|
||
|
||
if (!message) 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
|
||
const selectedMode = currentChatMode || 'auto';
|
||
|
||
// Add user message to chat
|
||
appendMessage('user', message);
|
||
clearInput();
|
||
|
||
// 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
|
||
function isShellCommand(message) {
|
||
const trimmed = message.trim().toLowerCase();
|
||
|
||
// Check for common shell command patterns
|
||
const commandPatterns = [
|
||
// Shell built-ins
|
||
/^(cd|ls|pwd|echo|cat|grep|find|rm|cp|mv|mkdir|rmdir|touch|chmod|chown|ln|head|tail|less|more|sort|uniq|wc|tar|zip|unzip|gzip|gunzip|df|du|ps|top|kill|killall|nohup|bg|fg|jobs|export|unset|env|source|\.|alias|unalias|history|clear|reset)(\s|$)/,
|
||
// Package managers
|
||
/^(npm|yarn|pnpm|pip|pip3|conda|brew|apt|apt-get|yum|dnf|pacman|curl|wget)(\s|$)/,
|
||
// Node.js commands
|
||
/^(node|npx)(\s|$)/,
|
||
// Python commands
|
||
/^(python|python3|pip|pip3|python3-m)(\s|$)/,
|
||
// Git commands
|
||
/^git(\s|$)/,
|
||
// Docker commands
|
||
/^docker(\s|$)/,
|
||
// File operations with paths
|
||
/^[a-z0-9_\-./]+\s*[\|>]/,
|
||
// Commands with arguments
|
||
/^[a-z][a-z0-9_\-]*\s+/,
|
||
// Absolute paths
|
||
/^\//,
|
||
// Scripts
|
||
/^(sh|bash|zsh|fish|powershell|pwsh)(\s|$)/
|
||
];
|
||
|
||
return commandPatterns.some(pattern => pattern.test(trimmed));
|
||
}
|
||
|
||
// Send shell command to active Claude CLI session
|
||
async function sendShellCommand(sessionId, command) {
|
||
try {
|
||
const response = await fetch('/claude/api/shell-command', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ sessionId, command })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
throw new Error(data.error || 'Command execution failed');
|
||
}
|
||
|
||
return data;
|
||
} 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;
|
||
}
|
||
|
||
try {
|
||
appendSystemMessage(`💻 Executing in session: <code>${message}</code>`);
|
||
|
||
// Send shell command to the active Claude CLI session
|
||
const result = await sendShellCommand(sessionId, message);
|
||
|
||
hideStreamingIndicator();
|
||
setGeneratingState(false);
|
||
|
||
// Show command output
|
||
if (result.stdout) {
|
||
appendMessage('assistant', result.stdout);
|
||
}
|
||
|
||
if (result.stderr) {
|
||
appendMessage('assistant', `<error>${result.stderr}</error>`);
|
||
}
|
||
|
||
// Show completion status
|
||
if (result.exitCode === 0) {
|
||
appendSystemMessage(`✅ Command completed successfully`);
|
||
} else {
|
||
appendSystemMessage(`⚠️ Command exited with code ${result.exitCode}`);
|
||
}
|
||
|
||
// Record tool usage
|
||
if (typeof contextPanel !== 'undefined' && contextPanel) {
|
||
contextPanel.recordToolUsage('shell_command');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Shell command execution failed:', error);
|
||
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; }
|
||
});
|
||
}
|