/** * Application bootstrap — state management, health polling, routing */ import { api } from './api.js'; import { initSidebar, renderSessions } from './components/sidebar.js'; import { initSessionForm } from './components/session-form.js'; import { initToolbar, updateToolbar } from './components/toolbar.js'; import { initChat, addMessage, setStreamingContent, clearChat, setInputEnabled, showExecutionIndicator, hideExecutionIndicator } from './components/chat.js'; import { initEventLog, addEvent, clearEvents } from './components/event-log.js'; import { initInspector, updateInspector, clearInspector } from './components/session-inspector.js'; import { initTimeline, addTimelineStep, updateTimelineStep, clearTimeline } from './components/agent-timeline.js'; // --- Global state --- export const state = { sessions: JSON.parse(localStorage.getItem('agentic_sessions') || '[]'), activeSessionId: null, sessionState: null, health: null, isExecuting: false, streamingContent: '', currentAgent: '', }; function saveSessions() { localStorage.setItem('agentic_sessions', JSON.stringify(state.sessions)); } // --- Session management --- export async function selectSession(sessionId) { api.unsubscribeAll(); state.activeSessionId = sessionId; state.streamingContent = ''; state.currentAgent = ''; clearChat(); clearEvents(); clearTimeline(); clearInspector(); if (!sessionId) { updateToolbar(null); renderSessions(); return; } location.hash = `session=${sessionId}`; renderSessions(); try { const session = await api.getSession(sessionId); state.sessionState = session; updateToolbar(session); updateInspector(session); const events = await api.getEvents(sessionId); for (const ev of events) addEvent(ev); api.subscribeSSE(sessionId); setInputEnabled(true); } catch (e) { addMessage('system', `Error loading session: ${e.message}`); } } export async function createSession(body) { const data = await api.createSession(body); if (!state.sessions.includes(data.session_id)) { state.sessions.unshift(data.session_id); saveSessions(); } await selectSession(data.session_id); return data; } export async function deleteSession(sessionId) { try { await api.deleteSession(sessionId); } catch { /* ignore 404 */ } api.unsubscribeSSE(sessionId); state.sessions = state.sessions.filter(s => s !== sessionId); saveSessions(); if (state.activeSessionId === sessionId) { state.activeSessionId = null; state.sessionState = null; location.hash = ''; clearChat(); clearEvents(); clearTimeline(); clearInspector(); updateToolbar(null); } renderSessions(); } export async function sendMessage(message, stream = false) { if (!state.activeSessionId || state.isExecuting) return; state.isExecuting = true; state.streamingContent = ''; setInputEnabled(false); addMessage('user', message); showExecutionIndicator(); try { if (stream) { await api.sendMessage(state.activeSessionId, message, true); // Response comes via SSE — handled by event listeners } else { const result = await api.sendMessage(state.activeSessionId, message, false); hideExecutionIndicator(); addMessage('assistant', result.content || JSON.stringify(result, null, 2)); state.isExecuting = false; setInputEnabled(true); // Refresh state const session = await api.getSession(state.activeSessionId); state.sessionState = session; updateToolbar(session); updateInspector(session); } } catch (e) { hideExecutionIndicator(); addMessage('system', `Error: ${e.message}`); state.isExecuting = false; setInputEnabled(true); } } // --- SSE event handlers --- function setupSSEListeners() { api.on('sse', (e) => { addEvent(e.detail); }); api.on('sse:execution.started', () => { state.isExecuting = true; showExecutionIndicator('Starting execution...'); }); api.on('sse:subagent.assigned', (e) => { const d = e.detail.data || e.detail; state.currentAgent = d.agent || ''; showExecutionIndicator(`Step ${d.step}/${d.total_steps} — ${d.agent}`); addTimelineStep({ step: d.step, totalSteps: d.total_steps, agent: d.agent, description: d.description, status: 'executing', }); }); api.on('sse:agent.delta', (e) => { const d = e.detail.data || e.detail; state.streamingContent += d.delta || ''; setStreamingContent(state.streamingContent, state.currentAgent); }); api.on('sse:tool.started', (e) => { const d = e.detail.data || e.detail; showExecutionIndicator(`Running tool: ${d.tool}`); }); api.on('sse:tool.completed', (e) => { const d = e.detail.data || e.detail; if (state.currentAgent) { updateTimelineStep(state.currentAgent, { tool: d.tool, status: d.status }); } }); api.on('sse:execution.completed', async (e) => { hideExecutionIndicator(); if (state.streamingContent) { // Finalize the streaming message addMessage('assistant', state.streamingContent); state.streamingContent = ''; } state.isExecuting = false; state.currentAgent = ''; setInputEnabled(true); // Refresh session state if (state.activeSessionId) { try { const session = await api.getSession(state.activeSessionId); state.sessionState = session; updateToolbar(session); updateInspector(session); } catch { /* ignore */ } } }); api.on('sse:error', (e) => { const d = e.detail.data || e.detail; addMessage('system', `Error: ${JSON.stringify(d)}`); hideExecutionIndicator(); state.isExecuting = false; setInputEnabled(true); }); } // --- Health polling --- let healthInterval; function startHealthPolling() { const poll = async () => { state.health = await api.checkHealth(); }; poll(); healthInterval = setInterval(poll, 10000); } // --- Theme --- export function toggleTheme() { const current = document.documentElement.getAttribute('data-theme'); const next = current === 'light' ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', next); localStorage.setItem('agentic_theme', next); } // --- Init --- function init() { // Restore theme const savedTheme = localStorage.getItem('agentic_theme') || 'dark'; if (savedTheme === 'light') { document.documentElement.setAttribute('data-theme', 'light'); } // Init components initToolbar(); initSidebar(); initSessionForm(); initChat(); initEventLog(); initInspector(); initTimeline(); // SSE listeners setupSSEListeners(); // Health startHealthPolling(); // Restore session from hash const hash = location.hash; const match = hash.match(/session=([a-f0-9]+)/); if (match && state.sessions.includes(match[1])) { selectSession(match[1]); } renderSessions(); } document.addEventListener('DOMContentLoaded', init);