Files
SuperCharged-Claude-Code-Up…/public/claude-ide/chat-functions.js
uroma efb3ecfb19 feat: AI auto-fix bug tracker with real-time error monitoring
- 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>
2026-01-21 10:53:11 +00:00

1214 lines
43 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================
// Chat Interface Functions
// ============================================
let chatSessionId = null;
let chatMessages = [];
let attachedSessionId = null;
let isGenerating = false; // Track if Claude is currently generating
let modeSuggestionTimeout = null; // Track mode suggestion auto-hide timer
// Reset all chat state
function resetChatState() {
console.log('Resetting chat state...');
chatSessionId = null;
chatMessages = [];
attachedSessionId = null;
console.log('Chat state reset complete');
}
// Load Chat View
async function loadChatView() {
console.log('[loadChatView] Loading chat view...');
// 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; }
});
}