// Simple client-side router with proper state management const router = { currentRoute: '/', refreshTimeout: null, detailRefreshTimeout: null, currentInstanceId: null, initialized: false, renderInProgress: false, init() { console.log('[Router] init() called'); if (this.initialized) { console.log('[Router] Already initialized, skipping'); return; } this.initialized = true; // Handle browser back/forward window.addEventListener('popstate', () => { console.log('[Router] popstate event'); this.handleRoute(window.location.pathname); }); // Handle initial route - call once after a short delay to ensure DOM is ready setTimeout(() => { console.log('[Router] Initial route handling'); this.handleRoute(window.location.pathname); }, 100); }, navigate(path) { console.log('[Router] navigate:', path); // Cancel any pending refreshes this.cancelRefreshes(); window.history.pushState({}, '', path); this.handleRoute(path); }, cancelRefreshes() { if (this.refreshTimeout) { console.log('[Router] Cancelling home refresh timeout'); clearTimeout(this.refreshTimeout); this.refreshTimeout = null; } if (this.detailRefreshTimeout) { console.log('[Router] Cancelling detail refresh timeout'); clearTimeout(this.detailRefreshTimeout); this.detailRefreshTimeout = null; } }, async handleRoute(path) { this.currentRoute = path; console.log('[Router] handleRoute:', path); const container = document.getElementById('page-container'); if (!container) { console.error('[Router] page-container not found!'); return; } // Cancel any pending refreshes when route changes this.cancelRefreshes(); if (path === '/' || path === '') { await this.renderHome(container); } else if (path.startsWith('/instance/')) { const id = path.split('/')[2]; await this.renderDetail(container, id); } else { container.innerHTML = components.error('Page not found'); } }, async renderHome(container) { console.log('[Router] renderHome called, renderInProgress:', this.renderInProgress); // Prevent concurrent renders if (this.renderInProgress) { console.log('[Router] Render already in progress, skipping'); return; } this.renderInProgress = true; try { console.log('[Router] Showing spinner'); container.innerHTML = components.spinner('Loading instances...'); console.log('[Router] Fetching instances from API'); const instances = await api.getInstances(); console.log('[Router] Received', instances.length, 'instances'); // Check if we're still on the home route (user might have navigated away) if (this.currentRoute !== '/' && this.currentRoute !== '') { console.log('[Router] Route changed during fetch, aborting render'); return; } // Check if we already have a container for instances let instancesList = container.querySelector('.instances-list'); const isInitialLoad = !instancesList; if (isInitialLoad) { instancesList = document.createElement('div'); instancesList.className = 'instances-list'; } if (instances.length === 0) { console.log('[Router] No instances, showing empty state'); container.innerHTML = components.emptyState( 'No running instances. Click "+ New Run" to start one.' ); } else { console.log('[Router] Building HTML for', instances.length, 'instances'); // Build a map of existing panels for efficient lookup const existingPanels = new Map(); if (!isInitialLoad) { instancesList.querySelectorAll('.instance-panel').forEach(panel => { const id = panel.getAttribute('data-id'); if (id) existingPanels.set(id, panel); }); } // Track which IDs we've seen const currentIds = new Set(); for (const instance of instances) { currentIds.add(instance.id); const stats = instance.stats || { total_tokens: 0, tool_calls: 0, errors: 0, duration_secs: 0 }; const newHtml = components.instancePanel(instance, stats, instance.latest_message); const existingPanel = existingPanels.get(instance.id); if (existingPanel) { // Update existing panel in-place const tempDiv = document.createElement('div'); tempDiv.innerHTML = newHtml; const newPanel = tempDiv.firstElementChild; existingPanel.replaceWith(newPanel); } else { // Add new panel const tempDiv = document.createElement('div'); tempDiv.innerHTML = newHtml; instancesList.appendChild(tempDiv.firstElementChild); } } // Remove panels for instances that no longer exist existingPanels.forEach((panel, id) => { if (!currentIds.has(id)) { panel.remove(); } }); if (isInitialLoad) { container.innerHTML = ''; container.appendChild(instancesList); } console.log('[Router] HTML set successfully'); } // Schedule next refresh only if still on home route if (this.currentRoute === '/' || this.currentRoute === '') { console.log('[Router] Scheduling auto-refresh in 5 seconds'); this.refreshTimeout = setTimeout(() => { console.log('[Router] Auto-refresh triggered'); this.renderHome(container); }, 5000); } } catch (error) { console.error('[Router] Error in renderHome:', error); container.innerHTML = components.error('Failed to load instances: ' + error.message); } finally { this.renderInProgress = false; console.log('[Router] renderHome complete, renderInProgress reset to false'); } }, async renderDetail(container, id) { console.log('[Router] renderDetail called for', id); this.currentInstanceId = id; // Check if we already have a detail view for this instance let detailView = container.querySelector('.detail-view'); const isInitialLoad = !detailView || detailView.getAttribute('data-instance-id') !== id; if (isInitialLoad) { container.innerHTML = components.spinner('Loading instance details...'); } try { const instance = await api.getInstance(id); const logs = await api.getInstanceLogs(id); // Check if we're still on this detail route if (this.currentRoute !== `/instance/${id}`) { console.log('[Router] Route changed during fetch, aborting render'); return; } // If not initial load, update in place if (!isInitialLoad) { detailView = container.querySelector('.detail-view'); if (detailView) { this.updateDetailView(detailView, instance, logs); // Schedule next refresh if (this.currentRoute === `/instance/${id}`) { this.detailRefreshTimeout = setTimeout(() => { this.renderDetail(container, id); }, 3000); } return; } } // Build detail view HTML let html = `
No tool calls yet
'; } html += `Loading...
${components.escapeHtml(data.content)}`;
// Apply syntax highlighting
content.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
} catch (error) {
content.innerHTML = ``;
}
};
// Close full file modal
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('full-file-close')?.addEventListener('click', () => {
document.getElementById('full-file-modal').classList.add('hidden');
});
});
// Expose to window for global access
window.router = router;