feat: persist active session across page navigation with inactivity TTL

Session ID is stored in localStorage keyed to user+persona. On page load
it's silently restored if within 30 min of last activity. Timestamp
updates on every sent message. New session / delete session clears the
stored ID so the TTL logic stays consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-26 23:38:04 -04:00
parent 4f09823afe
commit 92350f7a7b

View File

@@ -42,6 +42,31 @@
let currentHistory = []; // mirrors backend session [{role, content}, ...]
let talkThinkingDiv = null; // pending "thinking…" bubble for live Talk updates
// ── Session persistence ───────────────────────────────────────
// Survives page navigation (help, settings, etc.) within the same browser.
// Expires after SESSION_TTL_MS of inactivity.
const SESSION_TTL_MS = 30 * 60 * 1000; // 30 minutes
const _sid_key = `cx_sid_${CORTEX_USER}_${CORTEX_PERSONA}`;
const _sid_ts_key = `cx_sid_ts_${CORTEX_USER}_${CORTEX_PERSONA}`;
function persist_session() {
if (!sessionId) return;
localStorage.setItem(_sid_key, sessionId);
localStorage.setItem(_sid_ts_key, String(Date.now()));
}
function clear_stored_session() {
localStorage.removeItem(_sid_key);
localStorage.removeItem(_sid_ts_key);
}
function get_stored_session() {
const id = localStorage.getItem(_sid_key);
const ts = parseInt(localStorage.getItem(_sid_ts_key) || '0', 10);
if (!id || Date.now() - ts > SESSION_TTL_MS) return null;
return id;
}
// ── Enter toggle ─────────────────────────────────────────────
// Default: Ctrl+Enter sends. Stored in localStorage.
let ctrlEnterMode = localStorage.getItem('ctrlEnterSend') !== 'false';
@@ -314,6 +339,7 @@
const newItem = makeItem('new', '+ New session', '');
newItem.addEventListener('click', () => {
sessionId = null;
clear_stored_session();
currentHistory = [];
messagesEl.innerHTML = '';
sessionEl.textContent = '';
@@ -392,6 +418,7 @@
await fetch(`/sessions/${s.session_id}?${_fileParams}`, { method: 'DELETE' });
if (sessionId === s.session_id) {
sessionId = null;
clear_stored_session();
currentHistory = [];
messagesEl.innerHTML = '';
sessionEl.textContent = '';
@@ -425,10 +452,11 @@
return item;
}
async function resumeSession(id) {
async function resumeSession(id, silent = false) {
talkThinkingDiv = null;
if (id && id.startsWith('nct_')) sessionsBtn.classList.remove('talk-badge');
const res = await fetch(`/history/${id}?${_fileParams}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
messagesEl.innerHTML = '';
@@ -444,10 +472,11 @@
attachHistoryControls(msgDiv, i);
}
addMessage('system', `Resumed session ${id}`);
if (!silent) addMessage('system', `Resumed session ${id}`);
scrollToBottom();
sessionsPanel.classList.remove('open');
inputEl.focus();
persist_session();
}
function timeAgo(iso) {
@@ -793,6 +822,7 @@
if (data.type === 'response') {
sessionId = data.session_id;
sessionEl.textContent = `session: ${sessionId}`;
persist_session();
thinkingDiv.className = 'message assistant';
setMessageText(thinkingDiv, 'assistant', data.response);
const assistHistIdx = currentHistory.length;
@@ -893,6 +923,7 @@
if (job.session_id) {
sessionId = job.session_id;
sessionEl.textContent = `session: ${sessionId}`;
persist_session();
}
const userHistIdx = currentHistory.length - 1; // pushed before fetch
@@ -1315,3 +1346,10 @@
// and seed the mode UI (which also calls render_icons internally).
update_mode_ui();
render_icons();
// ── Auto-restore last session ─────────────────────────────────
// Silently resume if within the inactivity TTL; clears stored ID on error.
{
const stored = get_stored_session();
if (stored) resumeSession(stored, true).catch(clear_stored_session);
}