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:
@@ -42,6 +42,31 @@
|
|||||||
let currentHistory = []; // mirrors backend session [{role, content}, ...]
|
let currentHistory = []; // mirrors backend session [{role, content}, ...]
|
||||||
let talkThinkingDiv = null; // pending "thinking…" bubble for live Talk updates
|
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 ─────────────────────────────────────────────
|
// ── Enter toggle ─────────────────────────────────────────────
|
||||||
// Default: Ctrl+Enter sends. Stored in localStorage.
|
// Default: Ctrl+Enter sends. Stored in localStorage.
|
||||||
let ctrlEnterMode = localStorage.getItem('ctrlEnterSend') !== 'false';
|
let ctrlEnterMode = localStorage.getItem('ctrlEnterSend') !== 'false';
|
||||||
@@ -314,6 +339,7 @@
|
|||||||
const newItem = makeItem('new', '+ New session', '');
|
const newItem = makeItem('new', '+ New session', '');
|
||||||
newItem.addEventListener('click', () => {
|
newItem.addEventListener('click', () => {
|
||||||
sessionId = null;
|
sessionId = null;
|
||||||
|
clear_stored_session();
|
||||||
currentHistory = [];
|
currentHistory = [];
|
||||||
messagesEl.innerHTML = '';
|
messagesEl.innerHTML = '';
|
||||||
sessionEl.textContent = '';
|
sessionEl.textContent = '';
|
||||||
@@ -392,6 +418,7 @@
|
|||||||
await fetch(`/sessions/${s.session_id}?${_fileParams}`, { method: 'DELETE' });
|
await fetch(`/sessions/${s.session_id}?${_fileParams}`, { method: 'DELETE' });
|
||||||
if (sessionId === s.session_id) {
|
if (sessionId === s.session_id) {
|
||||||
sessionId = null;
|
sessionId = null;
|
||||||
|
clear_stored_session();
|
||||||
currentHistory = [];
|
currentHistory = [];
|
||||||
messagesEl.innerHTML = '';
|
messagesEl.innerHTML = '';
|
||||||
sessionEl.textContent = '';
|
sessionEl.textContent = '';
|
||||||
@@ -425,10 +452,11 @@
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resumeSession(id) {
|
async function resumeSession(id, silent = false) {
|
||||||
talkThinkingDiv = null;
|
talkThinkingDiv = null;
|
||||||
if (id && id.startsWith('nct_')) sessionsBtn.classList.remove('talk-badge');
|
if (id && id.startsWith('nct_')) sessionsBtn.classList.remove('talk-badge');
|
||||||
const res = await fetch(`/history/${id}?${_fileParams}`);
|
const res = await fetch(`/history/${id}?${_fileParams}`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
messagesEl.innerHTML = '';
|
messagesEl.innerHTML = '';
|
||||||
@@ -444,10 +472,11 @@
|
|||||||
attachHistoryControls(msgDiv, i);
|
attachHistoryControls(msgDiv, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
addMessage('system', `Resumed session ${id}`);
|
if (!silent) addMessage('system', `Resumed session ${id}`);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
sessionsPanel.classList.remove('open');
|
sessionsPanel.classList.remove('open');
|
||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
|
persist_session();
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(iso) {
|
function timeAgo(iso) {
|
||||||
@@ -793,6 +822,7 @@
|
|||||||
if (data.type === 'response') {
|
if (data.type === 'response') {
|
||||||
sessionId = data.session_id;
|
sessionId = data.session_id;
|
||||||
sessionEl.textContent = `session: ${sessionId}`;
|
sessionEl.textContent = `session: ${sessionId}`;
|
||||||
|
persist_session();
|
||||||
thinkingDiv.className = 'message assistant';
|
thinkingDiv.className = 'message assistant';
|
||||||
setMessageText(thinkingDiv, 'assistant', data.response);
|
setMessageText(thinkingDiv, 'assistant', data.response);
|
||||||
const assistHistIdx = currentHistory.length;
|
const assistHistIdx = currentHistory.length;
|
||||||
@@ -893,6 +923,7 @@
|
|||||||
if (job.session_id) {
|
if (job.session_id) {
|
||||||
sessionId = job.session_id;
|
sessionId = job.session_id;
|
||||||
sessionEl.textContent = `session: ${sessionId}`;
|
sessionEl.textContent = `session: ${sessionId}`;
|
||||||
|
persist_session();
|
||||||
}
|
}
|
||||||
|
|
||||||
const userHistIdx = currentHistory.length - 1; // pushed before fetch
|
const userHistIdx = currentHistory.length - 1; // pushed before fetch
|
||||||
@@ -1315,3 +1346,10 @@
|
|||||||
// and seed the mode UI (which also calls render_icons internally).
|
// and seed the mode UI (which also calls render_icons internally).
|
||||||
update_mode_ui();
|
update_mode_ui();
|
||||||
render_icons();
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user