From 22fce2fda14a6c1bae5fa80e3a2b6280c5f020a2 Mon Sep 17 00:00:00 2001 From: joaompfp Date: Sat, 2 May 2026 09:02:53 +0100 Subject: [PATCH 1/2] fix(sessions): handle 401 redirect gracefully in loadSession flow When the webui auth session expires (e.g., after a server restart), api() returns undefined after redirecting to /login. Previously, loadSession() and _ensureMessagesLoaded() would dereference the undefined response and throw, surfacing a confusing 'Failed to load session' toast while the browser was already navigating away. Add guards after api() calls that may trigger 401 redirects: - loadSession(): bail early if data is undefined - _ensureMessagesLoaded(): return silently if data is missing - _loadOlderMessages(): return silently if data is missing This prevents the stuck loading state and unnecessary error toasts when the user is already being redirected to re-authenticate. Fixes #1391 (reported as 'Failed to load session' after restart) --- static/sessions.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/static/sessions.js b/static/sessions.js index ed678e5f..9d9bd0fc 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -369,6 +369,12 @@ async function loadSession(sid){ if (_loadingSessionId === sid) _loadingSessionId = null; return; } + // Guard: api() may have redirected (401) and returned undefined; in that case + // the browser is already navigating away, so abort the rest of this flow. + if (!data) { + if (_loadingSessionId === sid) _loadingSessionId = null; + return; + } // Stale response? A newer loadSession() call has already started (#1060). if (_loadingSessionId !== sid) return; S.session=data.session; @@ -561,6 +567,8 @@ async function _ensureMessagesLoaded(sid) { } // Fetch session messages with a tail window for fast initial load. const data = await api(`/api/session?session_id=${encodeURIComponent(sid)}&messages=1&resolve_model=0&msg_limit=${_INITIAL_MSG_LIMIT}`); + // Guard: api() may have redirected (401) and returned undefined. + if (!data || !data.session) return; _messagesTruncated = !!data.session._messages_truncated; _oldestIdx = data.session._messages_offset || 0; const msgs = (data.session.messages || []).filter(m => m && m.role); @@ -601,7 +609,8 @@ async function _loadOlderMessages() { _loadingOlder = true; try { const data = await api(`/api/session?session_id=${encodeURIComponent(sid)}&messages=1&resolve_model=0&msg_before=${_oldestIdx}&msg_limit=${_INITIAL_MSG_LIMIT}`); - // Cancellation guards: + // Guard: api() may have redirected (401) and returned undefined. + if (!data || !data.session) { _loadingOlder = false; return; } // - response shape sane // - the active session is still the one we issued the request for. // Compare against S.session.session_id, NOT _loadingSessionId — the @@ -646,6 +655,8 @@ async function _ensureAllMessagesLoaded() { if (!_messagesTruncated || !S.session) return; const sid = S.session.session_id; const data = await api(`/api/session?session_id=${encodeURIComponent(sid)}&messages=1&resolve_model=0`); + // Guard: api() may have redirected (401) and returned undefined. + if (!data || !data.session) return; const msgs = (data.session.messages || []).filter(m => m && m.role); S.messages = msgs; _messagesTruncated = false; From eafda3cebc19970f7bdc81dccdfddecf056a8a83 Mon Sep 17 00:00:00 2001 From: joaompfp Date: Sat, 2 May 2026 17:30:01 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix(ui):=20model=20dropdown=20invisible=20o?= =?UTF-8?q?n=20mobile=20=E2=80=94=20anchor=20fallback=20to=20mobile=20acti?= =?UTF-8?q?on=20when=20desktop=20chip=20hidden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/ui.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/ui.js b/static/ui.js index db76d20f..953b3615 100644 --- a/static/ui.js +++ b/static/ui.js @@ -574,9 +574,10 @@ function _positionModelDropdown(){ const chip=$('composerModelChip'); const mobileAction=$('composerMobileModelAction'); const footer=document.querySelector('.composer-footer'); - if(!dd||!chip||!footer) return; + if(!dd||!footer) return; const panel=$('composerMobileConfigPanel'); - const anchor=(panel&&panel.classList.contains('open')&&mobileAction)?mobileAction:chip; + const anchor=(panel&&panel.classList.contains('open')&&mobileAction)?mobileAction:(chip&&chip.offsetParent?chip:mobileAction); + if(!anchor) return; const chipRect=anchor.getBoundingClientRect(); const footerRect=footer.getBoundingClientRect(); let left=chipRect.left-footerRect.left;