From 92350f7a7bb522371d7f2bec79d8c57a30718492 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Thu, 26 Mar 2026 23:38:04 -0400 Subject: [PATCH] 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 --- cortex/static/app.js | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/cortex/static/app.js b/cortex/static/app.js index ff12bdb..0288d18 100644 --- a/cortex/static/app.js +++ b/cortex/static/app.js @@ -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); + }