/** * Flynn Sessions Page * * Lists all sessions, allows viewing history and deleting sessions. */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } let _client = null; let _el = null; let _frontendFilter = ''; let _includeInactive = true; function formatTime(ts) { if (!ts || !Number.isFinite(ts)) {return '—';} try { return new Date(ts).toLocaleString(); } catch { return '—'; } } function formatQueue(config) { const queue = config?.queue; if (!queue?.mode) {return 'default';} const parts = [queue.mode]; if (typeof queue.debounceMs === 'number') { parts.push(`${queue.debounceMs}ms`); } if (typeof queue.cap === 'number') { parts.push(`cap:${queue.cap}`); } return parts.join(' · '); } async function loadSessionList() { if (!_client || !_el) {return;} const listContainer = _el.querySelector('#sessions-list'); const detailContainer = _el.querySelector('#session-detail'); if (detailContainer) {detailContainer.innerHTML = '';} try { const params = { includePersisted: _includeInactive, frontend: _frontendFilter || undefined, }; const result = await _client.call('sessions.list', params); const sessions = result.sessions ?? []; if (sessions.length === 0) { listContainer.innerHTML = '
No sessions found
'; return; } let html = `
`; for (const s of sessions) { html += ` `; } html += '
Session ID Frontend Messages Model Queue Last Activity Actions
${escapeHtml(s.id)} ${escapeHtml(s.frontend ?? (String(s.id).split(':')[0] || 'unknown'))} ${s.messageCount ?? 0} ${escapeHtml(s.config?.modelTier ?? 'default')} ${escapeHtml(formatQueue(s.config))} ${escapeHtml(formatTime(s.lastMessageAt))}
'; listContainer.innerHTML = html; // Bind view buttons listContainer.querySelectorAll('.session-view-btn, .session-view-link').forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); viewSession(btn.dataset.id); }); }); // Bind delete buttons listContainer.querySelectorAll('.session-delete-btn').forEach(btn => { btn.addEventListener('click', () => { deleteSession(btn.dataset.id); }); }); } catch (err) { listContainer.innerHTML = `
Failed to load sessions: ${err.message}
`; } } async function viewSession(sessionId) { const detailContainer = _el.querySelector('#session-detail'); if (!detailContainer) {return;} detailContainer.innerHTML = '
Loading...
'; try { const result = await _client.call('sessions.history', { sessionId }); const messages = result.messages ?? []; const roleClasses = { user: 'rounded-lg px-3 py-2 text-sm bg-blue-500/15 border border-blue-500/25 text-zinc-50', assistant: 'rounded-lg px-3 py-2 text-sm bg-zinc-900 border border-zinc-800 text-zinc-50', system: 'rounded-lg px-3 py-2 text-sm bg-zinc-800 text-zinc-400 border border-zinc-700', }; let html = `

${escapeHtml(sessionId)}

${messages.length} messages
`; if (messages.length === 0) { html += '
No messages in this session
'; } else { for (const msg of messages) { const role = msg.role ?? 'system'; const content = msg.content ?? msg.text ?? ''; const cls = roleClasses[role] ?? roleClasses.system; html += `
${escapeHtml(content)}
`; } } html += '
'; detailContainer.innerHTML = html; } catch (err) { detailContainer.innerHTML = `
Failed to load session: ${err.message}
`; } } async function deleteSession(sessionId) { if (!confirm(`Delete session "${sessionId}"? This will clear all message history.`)) { return; } try { await _client.call('sessions.delete', { sessionId }); await loadSessionList(); } catch (err) { alert(`Failed to delete session: ${err.message}`); } } export const SessionsPage = { async render(el, client) { _client = client; _el = el; el.innerHTML = `

Sessions

`; const frontendSelect = el.querySelector('#sessions-frontend-filter'); if (frontendSelect) { frontendSelect.value = _frontendFilter; frontendSelect.addEventListener('change', async () => { _frontendFilter = frontendSelect.value; await loadSessionList(); }); } const inactiveToggle = el.querySelector('#sessions-include-inactive'); if (inactiveToggle) { inactiveToggle.checked = _includeInactive; inactiveToggle.addEventListener('change', async () => { _includeInactive = inactiveToggle.checked; await loadSessionList(); }); } const refreshBtn = el.querySelector('#sessions-refresh-btn'); if (refreshBtn) { refreshBtn.addEventListener('click', async () => { await loadSessionList(); }); } await loadSessionList(); }, teardown() { _client = null; _el = null; }, };