From e72b9a8a95bc2ea4ace3b2c9d45cc38de54e1a81 Mon Sep 17 00:00:00 2001 From: ghreprimand Date: Tue, 2 Jun 2026 09:52:22 -0500 Subject: [PATCH] Fix stale deleted sessions in sidebar (#1203) Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com> --- static/app.js | 5 +-- static/js/sessions.js | 42 +++++++++++++++++-- ...test_deleted_session_sidebar_regression.py | 31 ++++++++++++++ 3 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 tests/test_deleted_session_sidebar_regression.py diff --git a/static/app.js b/static/app.js index a848193..d734ae1 100644 --- a/static/app.js +++ b/static/app.js @@ -3130,10 +3130,7 @@ function initializeEventListeners() { const idx = sessions.findIndex(s => s.id === currentId); const nextSession = sessions.filter(s => !s.archived && s.id !== currentId)[Math.max(0, idx)] || sessions.find(s => !s.archived && s.id !== currentId); - const res = await fetch(`${API_BASE}/api/session/${currentId}/archive`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); + const res = await fetch(`${API_BASE}/api/session/${currentId}`, { method: 'DELETE' }); if (res.ok) { await sessionModule.loadSessions(); if (nextSession) { diff --git a/static/js/sessions.js b/static/js/sessions.js index 24fd32a..c89206c 100644 --- a/static/js/sessions.js +++ b/static/js/sessions.js @@ -78,6 +78,42 @@ function _deselectCurrentSession(sid) { if (window._updateSendBtnIcon) window._updateSendBtnIcon(); } +function _removeSessionFromLocalState(sid) { + if (!sid) return; + const id = String(sid); + sessions = sessions.filter(s => String(s.id) !== id); + _selectedIds.delete(id); + try { + const savedOrder = Storage.get('session-order'); + if (savedOrder) { + const orderIds = JSON.parse(savedOrder); + if (Array.isArray(orderIds) && orderIds.some(x => String(x) === id)) { + Storage.set('session-order', JSON.stringify(orderIds.filter(x => String(x) !== id))); + } + } + } catch (e) { + console.warn('Failed to prune deleted session order:', e); + } + document.querySelectorAll('.list-item[data-session-id]').forEach(el => { + if (String(el.dataset.sessionId) === id) el.remove(); + }); + _deselectCurrentSession(id); +} + +function _normalizeSessionsList(fetched) { + if (!Array.isArray(fetched)) return []; + const seen = new Set(); + const unique = []; + for (const session of fetched) { + if (!session || session.id == null) continue; + const id = String(session.id); + if (seen.has(id)) continue; + seen.add(id); + unique.push(session); + } + return unique; +} + // Initialize dependencies from app.js (no-op: dependencies now imported directly) export function initDependencies() {} @@ -620,15 +656,13 @@ function createSessionItem(s) { _forceSidebarOpen(); return; } - // Optimistic: remove from UI immediately - const sessionEl = document.querySelector(`.list-item[data-session-id="${s.id}"]`); - if (sessionEl) sessionEl.remove(); const wasCurrentSession = currentSessionId === s.id; // If streaming, abort it before deleting if (wasCurrentSession && window.chatModule && window.chatModule.abortCurrentRequest) { window.chatModule.abortCurrentRequest(); } _deselectCurrentSession(s.id); + _removeSessionFromLocalState(s.id); _skipAutoSelect = true; // Clean up persistent chat mapping try { @@ -1321,7 +1355,7 @@ export async function loadSessions() { const res = await fetch(`${API_BASE}/api/sessions`); fetched = await res.json(); } - sessions = fetched; + sessions = _normalizeSessionsList(fetched); renderSessionList(); const sessionsSection = uiModule.el('sessions-section'); diff --git a/tests/test_deleted_session_sidebar_regression.py b/tests/test_deleted_session_sidebar_regression.py new file mode 100644 index 0000000..cf7d8de --- /dev/null +++ b/tests/test_deleted_session_sidebar_regression.py @@ -0,0 +1,31 @@ +from pathlib import Path + + +APP_JS = Path("static/app.js") +SESSIONS_JS = Path("static/js/sessions.js") + + +def test_rail_delete_uses_hard_delete_endpoint(): + source = APP_JS.read_text() + rail_block = source[source.index("const railDelete = el('rail-delete-session');"):] + rail_block = rail_block[:rail_block.index("// Textarea auto-resize")] + + assert "fetch(`${API_BASE}/api/session/${currentId}`, { method: 'DELETE' })" in rail_block + assert "api/session/${currentId}/archive" not in rail_block + + +def test_deleted_sessions_are_pruned_from_local_sidebar_state(): + source = SESSIONS_JS.read_text() + + assert "function _removeSessionFromLocalState(sid)" in source + assert "sessions = sessions.filter(s => String(s.id) !== id);" in source + assert "Storage.set('session-order', JSON.stringify(orderIds.filter(x => String(x) !== id)))" in source + assert "_removeSessionFromLocalState(s.id);" in source + + +def test_session_fetch_normalizes_duplicate_ids_before_render(): + source = SESSIONS_JS.read_text() + + assert "function _normalizeSessionsList(fetched)" in source + assert "if (seen.has(id)) continue;" in source + assert "sessions = _normalizeSessionsList(fetched);" in source