From 4559ca85f0ccb6d2e0d7a83198949351a5dab3ad Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sun, 5 Jul 2026 05:42:51 +0000 Subject: [PATCH] =?UTF-8?q?fix(#5409):=20iOS=20PWA=20session-switch=20?= =?UTF-8?q?=E2=80=94=20close=20sidebar=20instantly=20+=20prevent=20=5Fload?= =?UTF-8?q?ingSessionId=20race?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two iOS-PWA bugs when tapping an old/large session: 1) closeMobileSidebar() ran AFTER the slow `await _openSidebarSession()` (3-15s for large sessions on WKWebView), so the sidebar stayed open with only a tiny spinner. Moved closeMobileSidebar() to run synchronously BEFORE the await in the tap handler + lineage/child open paths for instant feedback. 2) _loadingSessionId race: an SSE-triggered idle reload (_scheduleActiveSessionIdleReload / refreshActiveSessionIfExternallyUpdated, fed by idle-reconcile/poll/visibility/focus) could overwrite _loadingSessionId and silently cancel an in-flight session switch. Both paths now skip while a different session's loadSession() is in flight. Contributor stage; gate-pass. Co-authored-by: luperrypf --- static/sessions.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/static/sessions.js b/static/sessions.js index 3fd49248..aa478e9f 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -674,6 +674,11 @@ function _scheduleActiveSessionIdleReload(sid) { if(!sid) return; setTimeout(async () => { if(!S||!S.session||S.session.session_id !== sid) return; + // #5409: skip idle reload while any loadSession() is in flight — avoids + // a race where the idle reload overwrites _loadingSessionId and silently + // cancels an in-progress session switch (most visible on iOS PWA with + // large sessions where Phase 1 metadata fetch is slow). + if(typeof _loadingSessionId !== 'undefined' && _loadingSessionId) return; if(S.busy || S.activeStreamId) return; if(typeof _isMessageReaderUnpinned==='function'&&_isMessageReaderUnpinned()){ _deferActiveSessionExternalRefresh('idle-reconcile'); @@ -5079,6 +5084,11 @@ async function refreshActiveSessionIfExternallyUpdated(reason){ // tradeoffs). const _recoveryReasons = {visible:true, focus:true}; const _keepStaleUntilLoaded = !!_recoveryReasons[String(reason||'')]; + // #5409: skip force-reload while a different session's loadSession() + // is in flight — avoids overwriting _loadingSessionId and silently + // cancelling an in-progress session switch. All four call paths + // (idle-reconcile, poll, visibility, focus) funnel through here. + if(typeof _loadingSessionId !== 'undefined' && _loadingSessionId && _loadingSessionId !== sid) return 'skipped'; await loadSession(sid, {force:true, externalRefreshReason:reason||'poll', keepStaleUntilLoaded:_keepStaleUntilLoaded}); if(typeof renderSessionList==='function') void renderSessionList(); return 'reloaded'; @@ -7337,6 +7347,8 @@ function renderSessionListFromCache(){ row.title=t('session_lineage_segment_open'); row.onclick=async(e)=>{ e.stopPropagation(); + // #5409: close mobile sidebar synchronously before navigation + if(typeof closeMobileSidebar==='function')closeMobileSidebar(); await _openSidebarSession(seg, {skipLineageResolve:true}); }; lineageList.appendChild(row); @@ -7349,6 +7361,8 @@ function renderSessionListFromCache(){ ['pointerdown','pointerup','click','touchstart','touchmove','touchend','touchcancel'].forEach(ev=>childList.addEventListener(ev,e=>e.stopPropagation())); const sortedChildren=[...s._child_sessions].sort((a,b)=>_sessionTimestampMs(b)-_sessionTimestampMs(a)); const openChildSession=async(childSession)=>{ + // #5409: close mobile sidebar synchronously before navigation + if(typeof closeMobileSidebar==='function')closeMobileSidebar(); await _openSidebarSession(childSession, {skipLineageResolve:true}); }; const childLabelFor=(child)=>{ @@ -7974,8 +7988,12 @@ function renderSessionListFromCache(){ if(_renamingSid) return; try{ if(($('sessionSearch').value||'').trim()) _hideSearchPreviewsAfterSelect=true; - await _openSidebarSession(s); + // #5409: close mobile sidebar synchronously BEFORE awaiting _openSidebarSession + // so the user gets instant feedback that navigation is happening, even + // for large sessions where loadSession can take 3-15s (metadata fetch + + // message load + renderMessages DOM build on slow iOS WKWebView). if(typeof closeMobileSidebar==='function')closeMobileSidebar(); + await _openSidebarSession(s); }finally{ el.classList.remove('loading'); }