diff --git a/CHANGELOG.md b/CHANGELOG.md index 42919130..8079ed45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Fixed + +- Session sidebar Archive/Delete menu actions now repaint from local sidebar state immediately after the server confirms the mutation, instead of waiting for the full `/api/sessions` refresh before the row disappears. + ## [v0.51.134] — 2026-05-25 — Release DF (stage-batch16 — single-PR Windows path defaults) ### Fixed diff --git a/static/sessions.js b/static/sessions.js index 83d8c5c4..34fb99ab 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -1544,6 +1544,24 @@ function _sessionArchiveToast(response, session){ function _sessionDeleteDescription(session){ return session&&session.worktree_path?t('session_delete_worktree_desc'):t('session_delete_desc'); } +function _optimisticallyArchiveSessionInList(sid, archived){ + if(!sid||!Array.isArray(_allSessions)) return; + let changed=false; + _allSessions=_allSessions.map(s=>{ + if(!s||s.session_id!==sid) return s; + changed=true; + return {...s,archived:!!archived}; + }); + if(changed) renderSessionListFromCache(); +} +function _optimisticallyRemoveSessionFromList(sid){ + if(!sid||!Array.isArray(_allSessions)) return; + const before=_allSessions.length; + _allSessions=_allSessions.filter(s=>!s||s.session_id!==sid); + if(_selectedSessions&&_selectedSessions.has(sid)) _selectedSessions.delete(sid); + if(typeof _dropStaleOptimisticSessionRow==='function') _dropStaleOptimisticSessionRow(sid); + if(_allSessions.length!==before) renderSessionListFromCache(); +} function _sessionIdFromLocation(){ if(typeof window==='undefined'||!window.location) return null; @@ -1866,9 +1884,10 @@ function _openSessionActionMenu(session, anchorEl){ closeSessionActionMenu(); try{ const response=await api('/api/session/archive',{method:'POST',body:JSON.stringify({session_id:session.session_id,archived:!session.archived})}); + _optimisticallyArchiveSessionInList(session.session_id,!session.archived); session.archived=!session.archived; if(S.session&&S.session.session_id===session.session_id) S.session.archived=session.archived; - await renderSessionList(); + void renderSessionList(); showToast(session.archived?_sessionArchiveToast(response,session):t('session_restored')); }catch(err){showToast(t('session_archive_failed')+err.message);} } @@ -1882,9 +1901,10 @@ function _openSessionActionMenu(session, anchorEl){ closeSessionActionMenu(); try{ await api('/api/session/archive',{method:'POST',body:JSON.stringify({session_id:session.session_id,archived:true})}); + _optimisticallyArchiveSessionInList(session.session_id,true); session.archived=true; if(S.session&&S.session.session_id===session.session_id) S.session.archived=true; - await renderSessionList(); + void renderSessionList(); showToast(t('session_hidden')); }catch(err){showToast(t('session_archive_failed')+err.message);} } @@ -3874,6 +3894,7 @@ async function deleteSession(sid){ let response=null; try{ response=await api('/api/session/delete',{method:'POST',body:JSON.stringify({session_id:sid})}); + _optimisticallyRemoveSessionFromList(sid); _clearHandoffStorageForSession(sid); }catch(e){setStatus(`Delete failed: ${e.message}`);return;} if(S.session&&S.session.session_id===sid){ @@ -3894,7 +3915,7 @@ async function deleteSession(sid){ } } showToast(_sessionResponseRetainsWorktree(response,session)?t('session_deleted_worktree'):t('session_deleted')); - await renderSessionList(); + void renderSessionList(); } // ── Project helpers ───────────────────────────────────────────────────── diff --git a/tests/test_session_action_menu_regression.py b/tests/test_session_action_menu_regression.py index e3d36cdc..136b5882 100644 --- a/tests/test_session_action_menu_regression.py +++ b/tests/test_session_action_menu_regression.py @@ -29,3 +29,29 @@ def test_session_list_refresh_does_not_close_open_conversation_actions(): assert "if(_renamingSid) return;" in body assert "if(_sessionActionMenu) return;" in body assert body.index("if(_sessionActionMenu) return;") < body.index("closeSessionActionMenu();") + + +def test_archive_action_repaints_sidebar_before_full_refresh(): + """Archive should hide the row from cached sidebar state before /api/sessions returns.""" + body = _function_block(SESSIONS_JS, "_openSessionActionMenu") + + api_call = "const response=await api('/api/session/archive'" + optimistic = "_optimisticallyArchiveSessionInList(session.session_id,!session.archived);" + full_refresh = "void renderSessionList();" + + assert optimistic in body + assert body.index(api_call) < body.index(optimistic) < body.index(full_refresh) + + +def test_delete_action_repaints_sidebar_before_loading_remaining_sessions(): + """Delete should remove the row locally before loading replacement session data.""" + body = _function_block(SESSIONS_JS, "deleteSession") + + api_call = "response=await api('/api/session/delete'" + optimistic = "_optimisticallyRemoveSessionFromList(sid);" + remaining_fetch = "const remaining=await api('/api/sessions');" + full_refresh = "void renderSessionList();" + + assert optimistic in body + assert body.index(api_call) < body.index(optimistic) < body.index(full_refresh) + assert body.index(optimistic) < body.index(remaining_fetch)