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>
This commit is contained in:
@@ -115,14 +115,22 @@ function connectWebSocket() {
|
||||
|
||||
window.ws = new WebSocket(wsUrl);
|
||||
|
||||
// Set ready state to connecting
|
||||
window.wsReady = false;
|
||||
|
||||
window.ws.onopen = () => {
|
||||
console.log('WebSocket connected, readyState:', window.ws.readyState);
|
||||
window.wsReady = true;
|
||||
|
||||
// Send a test message to verify connection
|
||||
try {
|
||||
window.ws.send(JSON.stringify({ type: 'ping' }));
|
||||
} catch (error) {
|
||||
console.error('Error sending ping:', error);
|
||||
}
|
||||
|
||||
// Flush any queued messages
|
||||
flushMessageQueue();
|
||||
};
|
||||
|
||||
window.ws.onmessage = (event) => {
|
||||
@@ -146,7 +154,7 @@ function connectWebSocket() {
|
||||
reason: event.reason,
|
||||
wasClean: event.wasClean
|
||||
});
|
||||
// Clear the ws reference
|
||||
window.wsReady = false;
|
||||
window.ws = null;
|
||||
// Attempt to reconnect after 5 seconds
|
||||
setTimeout(() => {
|
||||
@@ -156,6 +164,113 @@ function connectWebSocket() {
|
||||
};
|
||||
}
|
||||
|
||||
// === WebSocket State Management ===
|
||||
// Message queue for messages sent before WebSocket is ready
|
||||
window.messageQueue = [];
|
||||
window.wsReady = false;
|
||||
|
||||
/**
|
||||
* Queue a message to be sent when WebSocket is ready
|
||||
* @param {Object} message - Message to queue
|
||||
*/
|
||||
function queueMessage(message) {
|
||||
window.messageQueue.push({
|
||||
message: message,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
console.log(`[WebSocket] Message queued (${window.messageQueue.length} in queue):`, {
|
||||
type: message.type,
|
||||
sessionId: message.sessionId
|
||||
});
|
||||
showQueuedMessageIndicator();
|
||||
|
||||
// Try to flush immediately
|
||||
console.log('[WebSocket] Attempting immediate flush...');
|
||||
flushMessageQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush all queued messages to WebSocket
|
||||
*/
|
||||
function flushMessageQueue() {
|
||||
console.log('[WebSocket] flushMessageQueue called:', {
|
||||
wsReady: window.wsReady,
|
||||
wsExists: !!window.ws,
|
||||
wsReadyState: window.ws?.readyState,
|
||||
queueLength: window.messageQueue.length
|
||||
});
|
||||
|
||||
if (!window.wsReady || !window.ws) {
|
||||
console.log('[WebSocket] Not ready, keeping messages in queue');
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.messageQueue.length === 0) {
|
||||
console.log('[WebSocket] Queue is empty, nothing to flush');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[WebSocket] Flushing ${window.messageQueue.length} queued messages`);
|
||||
|
||||
// Send all queued messages
|
||||
const messagesToSend = [...window.messageQueue];
|
||||
window.messageQueue = [];
|
||||
|
||||
for (const item of messagesToSend) {
|
||||
try {
|
||||
const payloadStr = JSON.stringify(item.message);
|
||||
console.log('[WebSocket] Sending queued message:', {
|
||||
type: item.message.type,
|
||||
sessionId: item.message.sessionId,
|
||||
payloadLength: payloadStr.length
|
||||
});
|
||||
window.ws.send(payloadStr);
|
||||
console.log('[WebSocket] ✓ Sent queued message:', item.message.type);
|
||||
} catch (error) {
|
||||
console.error('[WebSocket] ✗ Failed to send queued message:', error);
|
||||
// Put it back in the queue
|
||||
window.messageQueue.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
hideQueuedMessageIndicator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show indicator that messages are queued
|
||||
*/
|
||||
function showQueuedMessageIndicator() {
|
||||
let indicator = document.getElementById('queued-message-indicator');
|
||||
if (!indicator) {
|
||||
indicator = document.createElement('div');
|
||||
indicator.id = 'queued-message-indicator';
|
||||
indicator.className = 'queued-message-indicator';
|
||||
indicator.innerHTML = `
|
||||
<span class="indicator-icon">⏳</span>
|
||||
<span class="indicator-text">Message queued...</span>
|
||||
`;
|
||||
|
||||
// Add to chat input area
|
||||
const chatContainer = document.getElementById('chat-input-container');
|
||||
if (chatContainer) {
|
||||
chatContainer.appendChild(indicator);
|
||||
}
|
||||
}
|
||||
|
||||
indicator.style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide queued message indicator
|
||||
*/
|
||||
function hideQueuedMessageIndicator() {
|
||||
const indicator = document.getElementById('queued-message-indicator');
|
||||
if (indicator && window.messageQueue.length === 0) {
|
||||
indicator.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function handleWebSocketMessage(data) {
|
||||
switch(data.type) {
|
||||
case 'connected':
|
||||
@@ -275,6 +390,11 @@ function handleOperationProgress(data) {
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming message state for accumulating response chunks
|
||||
let streamingMessageElement = null;
|
||||
let streamingMessageContent = '';
|
||||
let streamingTimeout = null;
|
||||
|
||||
function handleSessionOutput(data) {
|
||||
// Handle output for sessions view
|
||||
if (currentSession && data.sessionId === currentSession.id) {
|
||||
@@ -283,15 +403,40 @@ function handleSessionOutput(data) {
|
||||
|
||||
// Handle output for chat view
|
||||
if (typeof attachedSessionId !== 'undefined' && data.sessionId === attachedSessionId) {
|
||||
// Hide streaming indicator
|
||||
// Hide streaming indicator on first chunk
|
||||
if (typeof hideStreamingIndicator === 'function') {
|
||||
hideStreamingIndicator();
|
||||
}
|
||||
|
||||
// Append output as assistant message
|
||||
if (typeof appendMessage === 'function') {
|
||||
appendMessage('assistant', data.data.content, true);
|
||||
const content = data.data.content || '';
|
||||
|
||||
// Accumulate streaming content
|
||||
if (streamingMessageElement && streamingMessageElement.isConnected) {
|
||||
// Append to existing message
|
||||
streamingMessageContent += content;
|
||||
const bubble = streamingMessageElement.querySelector('.chat-message-bubble');
|
||||
if (bubble && typeof formatMessage === 'function') {
|
||||
bubble.innerHTML = formatMessage(streamingMessageContent);
|
||||
}
|
||||
} else {
|
||||
// Start new streaming message
|
||||
streamingMessageContent = content;
|
||||
if (typeof appendMessage === 'function') {
|
||||
appendMessage('assistant', content, true);
|
||||
// Get the message element we just created
|
||||
streamingMessageElement = document.querySelector('.chat-message.assistant:last-child');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset streaming timeout - if no new chunks for 1 second, consider stream complete
|
||||
clearTimeout(streamingTimeout);
|
||||
streamingTimeout = setTimeout(() => {
|
||||
streamingMessageElement = null;
|
||||
streamingMessageContent = '';
|
||||
if (typeof setGeneratingState === 'function') {
|
||||
setGeneratingState(false);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,6 +792,7 @@ async function continueSessionInChat(sessionId) {
|
||||
window.pendingSessionId = sessionId;
|
||||
window.pendingSessionData = session;
|
||||
|
||||
hideLoadingOverlay();
|
||||
switchView('chat');
|
||||
|
||||
} catch (error) {
|
||||
@@ -855,13 +1001,13 @@ async function loadFile(filePath) {
|
||||
const res = await fetch(`/claude/api/file/${encodeURIComponent(filePath)}`);
|
||||
const data = await res.json();
|
||||
|
||||
// Check if FileEditor component is available
|
||||
if (window.fileEditor) {
|
||||
// Use the new CodeMirror-based editor
|
||||
await window.fileEditor.openFile(filePath, data.content || '');
|
||||
// Check if Monaco Editor component is available
|
||||
if (window.monacoEditor) {
|
||||
// Use the Monaco-based editor
|
||||
await window.monacoEditor.openFile(filePath, data.content || '');
|
||||
} else {
|
||||
// Fallback to the old view if FileEditor is not loaded yet
|
||||
console.warn('[loadFile] FileEditor not available, using fallback');
|
||||
// Fallback to simple view
|
||||
console.warn('[loadFile] Monaco Editor not available, using fallback');
|
||||
const editorEl = document.getElementById('file-editor');
|
||||
|
||||
const isHtmlFile = filePath.toLowerCase().endsWith('.html') || filePath.toLowerCase().endsWith('.htm');
|
||||
@@ -872,8 +1018,7 @@ async function loadFile(filePath) {
|
||||
<div class="file-header">
|
||||
<h2>${filePath}</h2>
|
||||
<div class="file-actions">
|
||||
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
|
||||
<button class="btn-primary btn-sm" onclick="showHtmlPreview('${filePath}')">👁️ Preview</button>
|
||||
<button class="btn-secondary btn-sm" onclick="showHtmlPreview('${filePath}')">👁️ Preview</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-content" id="file-content-view">
|
||||
@@ -882,7 +1027,7 @@ async function loadFile(filePath) {
|
||||
<button class="toggle-btn" data-view="preview" onclick="switchFileView('preview')">Preview</button>
|
||||
</div>
|
||||
<div class="code-view">
|
||||
<pre><code class="language-html">${escapeHtml(data.content)}</code></pre>
|
||||
<pre><code class="language-html">${escapeHtml(data.content || '')}</code></pre>
|
||||
</div>
|
||||
<div class="preview-view" style="display: none;">
|
||||
<iframe id="html-preview-frame" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
|
||||
@@ -891,7 +1036,7 @@ async function loadFile(filePath) {
|
||||
`;
|
||||
|
||||
// Store file content for preview
|
||||
window.currentFileContent = data.content;
|
||||
window.currentFileContent = data.content || '';
|
||||
window.currentFilePath = filePath;
|
||||
|
||||
// Highlight code
|
||||
@@ -902,12 +1047,14 @@ async function loadFile(filePath) {
|
||||
}
|
||||
} else {
|
||||
// Non-HTML file - show content
|
||||
const language = getLanguageFromFile(filePath);
|
||||
editorEl.innerHTML = `
|
||||
<div class="file-header">
|
||||
<h2>${filePath}</h2>
|
||||
<span class="language-badge">${language}</span>
|
||||
</div>
|
||||
<div class="file-content">
|
||||
<pre><code>${escapeHtml(data.content || '')}</code></pre>
|
||||
<pre class="code-content"><code>${escapeHtml(data.content || '')}</code></pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -926,6 +1073,24 @@ async function loadFile(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get language from file path
|
||||
function getLanguageFromFile(filePath) {
|
||||
const ext = filePath.split('.').pop().toLowerCase();
|
||||
const languageMap = {
|
||||
'js': 'JavaScript',
|
||||
'jsx': 'JavaScript JSX',
|
||||
'ts': 'TypeScript',
|
||||
'tsx': 'TypeScript JSX',
|
||||
'py': 'Python',
|
||||
'html': 'HTML',
|
||||
'css': 'CSS',
|
||||
'json': 'JSON',
|
||||
'md': 'Markdown',
|
||||
'txt': 'Plain Text'
|
||||
};
|
||||
return languageMap[ext] || 'Plain Text';
|
||||
}
|
||||
|
||||
async function loadFileContent(filePath) {
|
||||
await loadFile(filePath);
|
||||
switchView('files');
|
||||
|
||||
Reference in New Issue
Block a user