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

@@ -0,0 +1,435 @@
/**
* Session Picker Component
* Show modal on startup to select existing session or create new
*
* Features:
* - Session picker modal on startup
* - Recent sessions list
* - Sessions grouped by project
* - Create new session
* - Session forking support
*/
class SessionPicker {
constructor() {
this.modal = null;
this.sessions = [];
this.initialized = false;
}
async initialize() {
if (this.initialized) return;
// Check URL params first
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get('session');
const project = urlParams.get('project');
if (sessionId) {
// Load specific session
console.log('[SessionPicker] Loading session from URL:', sessionId);
await this.loadSession(sessionId);
this.initialized = true;
return;
}
if (project) {
// Create or load session for project
console.log('[SessionPicker] Project context:', project);
await this.ensureSessionForProject(project);
this.initialized = true;
return;
}
// No session or project - show picker
await this.showPicker();
this.initialized = true;
}
async showPicker() {
// Create modal
this.modal = document.createElement('div');
this.modal.className = 'session-picker-modal';
this.modal.innerHTML = `
<div class="session-picker-content">
<div class="picker-header">
<h2>Select a Session</h2>
<button class="btn-close" onclick="window.sessionPicker.close()">×</button>
</div>
<div class="picker-tabs">
<button class="picker-tab active" data-tab="recent" onclick="window.sessionPicker.switchTab('recent')">
<span class="tab-icon">🕐</span>
<span class="tab-label">Recent</span>
</button>
<button class="picker-tab" data-tab="projects" onclick="window.sessionPicker.switchTab('projects')">
<span class="tab-icon">📁</span>
<span class="tab-label">Projects</span>
</button>
<button class="picker-tab" data-tab="new" onclick="window.sessionPicker.switchTab('new')">
<span class="tab-icon"></span>
<span class="tab-label">New Session</span>
</button>
</div>
<div class="picker-body">
<div id="picker-recent" class="picker-tab-content active">
<div class="loading">Loading recent sessions...</div>
</div>
<div id="picker-projects" class="picker-tab-content">
<div class="loading">Loading projects...</div>
</div>
<div id="picker-new" class="picker-tab-content">
<div class="new-session-form">
<div class="form-group">
<label>Session Name</label>
<input type="text" id="new-session-name" placeholder="My Session" />
</div>
<div class="form-group">
<label>Project (optional)</label>
<input type="text" id="new-session-project" placeholder="my-project" />
</div>
<button class="btn-primary btn-block" onclick="window.sessionPicker.createNewSession()">
Create Session
</button>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(this.modal);
document.body.style.overflow = 'hidden'; // Prevent scrolling
// Load recent sessions
await this.loadRecentSessions();
await this.loadProjects();
}
async loadRecentSessions() {
const container = document.getElementById('picker-recent');
try {
const response = await fetch('/claude/api/claude/sessions');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
this.sessions = data.sessions || [];
if (this.sessions.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-icon">💬</div>
<h3>No sessions yet</h3>
<p>Create a new session to get started</p>
<button class="btn-primary" onclick="window.sessionPicker.switchTab('new')">
Create Session
</button>
</div>
`;
return;
}
// Sort by last modified
this.sessions.sort((a, b) => {
const dateA = new Date(a.modified || a.created);
const dateB = new Date(b.modified || b.created);
return dateB - dateA;
});
// Show last 10 sessions
const recentSessions = this.sessions.slice(0, 10);
container.innerHTML = recentSessions.map(session => {
const date = new Date(session.modified || session.created);
const timeAgo = this.formatTimeAgo(date);
const title = session.title || session.id;
const project = session.project || 'General';
return `
<div class="session-item" onclick="window.sessionPicker.selectSession('${session.id}')">
<div class="session-icon">💬</div>
<div class="session-info">
<div class="session-title">${this.escapeHtml(title)}</div>
<div class="session-meta">
<span class="session-project">${this.escapeHtml(project)}</span>
<span class="session-time">${timeAgo}</span>
</div>
</div>
<div class="session-arrow">→</div>
</div>
`;
}).join('');
} catch (error) {
console.error('[SessionPicker] Failed to load sessions:', error);
container.innerHTML = `
<div class="error-state">
<h3>Failed to load sessions</h3>
<p>${error.message}</p>
<button class="btn-secondary" onclick="window.sessionPicker.loadRecentSessions()">
Try Again
</button>
</div>
`;
}
}
async loadProjects() {
const container = document.getElementById('picker-projects');
try {
// Use the sessions endpoint to get projects
const response = await fetch('/claude/api/claude/sessions');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// Group sessions by project
const projectMap = new Map();
const allSessions = [
...(data.active || []),
...(data.historical || [])
];
allSessions.forEach(session => {
const projectName = session.metadata?.project || session.workingDir?.split('/').pop() || 'Untitled';
if (!projectMap.has(projectName)) {
projectMap.set(projectName, {
name: projectName,
sessionCount: 0,
lastSession: session
});
}
const project = projectMap.get(projectName);
project.sessionCount++;
});
const projects = Array.from(projectMap.values());
if (projects.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-icon">📁</div>
<h3>No projects yet</h3>
<p>Create a new project to organize your sessions</p>
<button class="btn-primary" onclick="window.sessionPicker.switchTab('new')">
New Session
</button>
</div>
`;
return;
}
// Sort by session count (most used first)
projects.sort((a, b) => b.sessionCount - a.sessionCount);
container.innerHTML = projects.map(project => {
const sessionCount = project.sessionCount || 0;
return `
<div class="project-item" onclick="window.sessionPicker.selectProject('${this.escapeHtml(project.name)}')">
<div class="project-icon">📁</div>
<div class="project-info">
<div class="project-name">${this.escapeHtml(project.name)}</div>
<div class="project-meta">${sessionCount} session${sessionCount !== 1 ? 's' : ''}</div>
</div>
<div class="project-arrow">→</div>
</div>
`;
}).join('');
} catch (error) {
console.error('[SessionPicker] Failed to load projects:', error);
container.innerHTML = `
<div class="error-state">
<h3>Failed to load projects</h3>
<p>${error.message}</p>
</div>
`;
}
}
async selectSession(sessionId) {
await this.loadSession(sessionId);
this.close();
}
async selectProject(projectName) {
await this.ensureSessionForProject(projectName);
this.close();
}
async loadSession(sessionId) {
try {
const response = await fetch(`/claude/api/claude/sessions/${sessionId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const session = await response.json();
// Attach to session
if (typeof attachToSession === 'function') {
attachToSession(sessionId);
}
console.log('[SessionPicker] Loaded session:', sessionId);
return session;
} catch (error) {
console.error('[SessionPicker] Failed to load session:', error);
if (typeof showToast === 'function') {
showToast(`Failed to load session: ${error.message}`, 'error', 3000);
}
}
}
async ensureSessionForProject(projectName) {
try {
// Check if session exists for this project
const response = await fetch('/claude/api/claude/sessions');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const sessions = data.sessions || [];
const projectSession = sessions.find(s => s.project === projectName);
if (projectSession) {
return await this.loadSession(projectSession.id);
}
// Create new session for project
return await this.createNewSession(projectName);
} catch (error) {
console.error('[SessionPicker] Failed to ensure session:', error);
}
}
async createNewSession(projectName = null) {
const nameInput = document.getElementById('new-session-name');
const projectInput = document.getElementById('new-session-project');
const name = nameInput?.value || projectName || 'Untitled Session';
const project = projectInput?.value || projectName || '';
try {
const response = await fetch('/claude/api/claude/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: name,
project: project,
source: 'web-ide'
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const session = await response.json();
// Attach to new session
if (typeof attachToSession === 'function') {
attachToSession(session.id);
}
console.log('[SessionPicker] Created session:', session.id);
this.close();
return session;
} catch (error) {
console.error('[SessionPicker] Failed to create session:', error);
if (typeof showToast === 'function') {
showToast(`Failed to create session: ${error.message}`, 'error', 3000);
}
}
}
switchTab(tabName) {
// Update tab buttons
this.modal.querySelectorAll('.picker-tab').forEach(tab => {
tab.classList.remove('active');
if (tab.dataset.tab === tabName) {
tab.classList.add('active');
}
});
// Update tab content
this.modal.querySelectorAll('.picker-tab-content').forEach(content => {
content.classList.remove('active');
});
const activeContent = document.getElementById(`picker-${tabName}`);
if (activeContent) {
activeContent.classList.add('active');
}
}
close() {
if (this.modal) {
this.modal.remove();
this.modal = null;
}
document.body.style.overflow = ''; // Restore scrolling
}
formatTimeAgo(date) {
const seconds = Math.floor((new Date() - date) / 1000);
if (seconds < 60) {
return 'Just now';
} else if (seconds < 3600) {
const minutes = Math.floor(seconds / 60);
return `${minutes}m ago`;
} else if (seconds < 86400) {
const hours = Math.floor(seconds / 3600);
return `${hours}h ago`;
} else if (seconds < 604800) {
const days = Math.floor(seconds / 86400);
return `${days}d ago`;
} else {
return date.toLocaleDateString();
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Global instance
let sessionPicker = null;
// Auto-initialize
if (typeof window !== 'undefined') {
window.SessionPicker = SessionPicker;
// Create instance
sessionPicker = new SessionPicker();
window.sessionPicker = sessionPicker;
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
sessionPicker.initialize();
});
} else {
sessionPicker.initialize();
}
}
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = { SessionPicker };
}