fix: preserve first-turn sidebar row during refresh

This commit is contained in:
Michael Lam
2026-05-06 23:30:01 -07:00
committed by nesquena-hermes
parent 5e01b00b8b
commit 20861b6721
3 changed files with 65 additions and 1 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -1450,6 +1450,53 @@ window.addEventListener('resize',()=>{
// with stale data, causing sessions to vanish from the sidebar.
let _renderSessionListGen = 0;
function _isOptimisticFirstTurnSessionRow(s){
if(!s||!s.session_id||s.archived) return false;
const messageCount=Number(s.message_count||0);
if(messageCount<=0&&!s.pending_user_message) return false;
return Boolean(
s.is_streaming||
s.active_stream_id||
s.pending_user_message||
s.pending_started_at||
_isSessionLocallyStreaming(s)||
_sessionStreamingById.get(s.session_id)===true
);
}
function _mergeOptimisticFirstTurnSessions(fetchedSessions){
const merged=Array.isArray(fetchedSessions)?[...fetchedSessions]:[];
const bySid=new Map();
merged.forEach((s,idx)=>{if(s&&s.session_id) bySid.set(s.session_id,idx);});
for(const local of Array.isArray(_allSessions)?_allSessions:[]){
if(!_isOptimisticFirstTurnSessionRow(local)) continue;
const sid=local.session_id;
const idx=bySid.has(sid)?bySid.get(sid):-1;
if(idx>=0){
const fetched=merged[idx]||{};
const localCount=Number(local.message_count||0);
const fetchedCount=Number(fetched.message_count||0);
const localTs=Number(local.last_message_at||local.updated_at||0);
const fetchedTs=Number(fetched.last_message_at||fetched.updated_at||0);
merged[idx]={
...local,
...fetched,
message_count:Math.max(localCount,fetchedCount),
last_message_at:Math.max(localTs,fetchedTs),
updated_at:Math.max(Number(local.updated_at||0),Number(fetched.updated_at||0),localTs,fetchedTs),
active_stream_id:fetched.active_stream_id||local.active_stream_id||null,
pending_user_message:fetched.pending_user_message||local.pending_user_message||null,
pending_started_at:fetched.pending_started_at||local.pending_started_at||null,
is_streaming:Boolean(fetched.is_streaming||local.is_streaming||_isSessionLocallyStreaming(local)),
};
}else{
merged.push({...local,is_streaming:true});
bySid.set(sid,merged.length-1);
}
}
return merged;
}
async function renderSessionList(){
const _gen = ++_renderSessionListGen;
try{
@@ -1465,7 +1512,7 @@ async function renderSessionList(){
// active profile so the "Show N from other profiles" toggle can render
// without a second round-trip. Stashed on the module for renderSessionListFromCache.
_otherProfileCount = sessData.other_profile_count || 0;
_allSessions = sessData.sessions||[];
_allSessions = _mergeOptimisticFirstTurnSessions(sessData.sessions||[]);
_allProjects = projData.projects||[];
// Capture server clock for clock-skew compensation (issue #1144).
// server_time is epoch seconds from the server's time.time().

View File

@@ -97,3 +97,20 @@ class TestSidebarFirstTurnVisibility:
"The index-path empty-session filter must exempt pending first-turn sessions, "
"matching the full-scan fallback."
)
def test_session_refresh_preserves_optimistic_first_turn_rows_when_server_lags(self):
src = read("static/sessions.js")
assert "function _mergeOptimisticFirstTurnSessions" in src, (
"renderSessionList() must merge locally optimistically inserted first-turn rows "
"back into the fetched /api/sessions result. A session switch can re-fetch before "
"the server has saved pending state, and replacing _allSessions would hide the "
"new in-flight chat until the stream finishes."
)
render_start = src.index("async function renderSessionList")
render_end = src.index("// ── Gateway session SSE", render_start)
render_body = src[render_start:render_end]
assign_idx = render_body.index("_allSessions =")
assert "_mergeOptimisticFirstTurnSessions" in render_body[:assign_idx + 160], (
"The fetched session list should be merged with optimistic rows at the assignment "
"site, before completion transitions or renderSessionListFromCache() run."
)