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:
uroma
2026-01-21 10:53:11 +00:00
Unverified
parent b765c537fc
commit efb3ecfb19
23 changed files with 7254 additions and 119 deletions

View File

@@ -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');