/**
* 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 = `
| Session ID |
Frontend |
Messages |
Model |
Queue |
Last Activity |
Actions |
`;
for (const s of sessions) {
html += `
| ${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))} |
|
`;
}
html += '
';
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;
},
};