From dbeed4b63f2169da5365a95ca402930bb9648455 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:37:21 +0100 Subject: [PATCH] perf(ui): stop session loading from blocking shell (#5927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(ui): stop session loading from blocking shell * fix(startup): open routes on their own data, retire the loader for good Follow-up to review on #5927. - Route openers are now classified by the data they actually read. Only /email touches the hydrated session list (its new-chat path falls back to the most recent session's model when no default chat is set), so every other route opens as soon as module wiring completes instead of queueing behind /api/sessions. This is the deferred-route half of #5926, which the first pass left unimplemented. - index.html's 5s fallback removes the loader node again. Leaving it in the DOM indefinitely kept _shouldPreserveStartupComposer true forever on a hung /api/sessions, so the composer stopped clearing on session switch. - A missing session module settles hydration instead of leaving the sidebar on "Loading chats…" and dropping the user's route on the floor. - Startup sequencing moved to static/js/startupShell.js so it can be run by tests. The source-text assertions in test_startup_shell_session_loading.py are replaced by node-driven behavioural tests, per tests/TESTING_STANDARD.md. - Reverted the unrequested loader a11y rework, removed the duplicated inert writes (the module stops the wave interval through a callback), and moved the bootstrap row's inline styles into .session-list-bootstrap. * fix: preserve session bootstrap failure state --------- Co-authored-by: Léo --- static/app.js | 47 +-- static/index.html | 22 +- static/js/sessions.js | 21 +- static/js/startupShell.js | 153 +++++++++ static/style.css | 6 + tests/test_startup_session_bootstrap_js.py | 356 +++++++++++++++++++ tests/test_startup_shell_js.py | 377 +++++++++++++++++++++ 7 files changed, 957 insertions(+), 25 deletions(-) create mode 100644 static/js/startupShell.js create mode 100644 tests/test_startup_session_bootstrap_js.py create mode 100644 tests/test_startup_shell_js.py diff --git a/static/app.js b/static/app.js index 2f1e8d4bf..514d59d8b 100644 --- a/static/app.js +++ b/static/app.js @@ -15,6 +15,12 @@ import compareModule from './js/compare/index.js?v=20260723compareicon2'; import documentModule from './js/document.js?v=20260722emailfastindex1'; import searchChatModule from './js/search-chat.js'; import { makeWindowDraggable } from './js/windowDrag.js'; +import { + revealApplicationShellAfterPaint, + runDeferredRouteOpener, + deferRouteOpener, + settleSessionHydration +} from './js/startupShell.js'; import markdownModule from './js/markdown.js'; import chatRenderer from './js/chatRenderer.js?v=20260722emailfastindex1'; import sessionModule from './js/sessions.js'; @@ -1217,12 +1223,13 @@ function initializeEventListeners() { '/library': () => sessionModule && sessionModule.openLibrary && sessionModule.openLibrary(), }; const _opener = _routeOpen[urlPath]; - // Defer the opener — at this point in init, the modules whose handlers - // we trigger (#rail-new-session click handler, the email-section header - // click handler in emailInbox, sessionModule's loaded session list) are - // still being wired up further down in this same function. Stash the - // opener so it runs from sessionModule.loadSessions().finally() below. - if (_opener) window._odysseusRouteOpener = _opener; + // Defer the opener — at this point in init, the modules whose handlers we + // trigger (#rail-new-session click handler, the email-section header click + // handler in emailInbox, sessionModule) are still being wired up further + // down in this same function. startupShell decides when it can run: as soon + // as wiring completes, or — for the routes that read the session list — + // once /api/sessions has settled. + deferRouteOpener(urlPath, _opener); // Archive browser tool button const toolLibraryBtn = el('tool-library-btn'); @@ -4315,6 +4322,10 @@ function startOdysseusApp() { // Load initial data presetsModule.loadPresets(uiModule.showError); + // Core wiring is complete for this turn — reveal the shell independently of + // the session-list request. + revealApplicationShellAfterPaint(); + if (sessionModule) { sessionModule.initDependencies({ API_BASE: API_BASE, @@ -4326,21 +4337,19 @@ function startOdysseusApp() { scrollHistory: uiModule.scrollHistoryInstant }); - // Load sessions first (critical path) — remove loader when done - sessionModule.loadSessions() - .catch(e => console.warn('loadSessions error:', e)) - .finally(() => { - const loader = document.getElementById('app-loader'); - if (loader) { loader.style.opacity = '0'; setTimeout(() => loader.remove(), 300); } - // Fire any URL route opener now that sessions + module wiring are - // ready. Deferred from up top of init for exactly this reason. - if (window._odysseusRouteOpener) { - try { window._odysseusRouteOpener(); } catch (_) {} - window._odysseusRouteOpener = null; - } - }); + // sessionModule is now wired, so every route opener has the modules it + // drives. The ones that read no session data open here rather than + // queueing behind /api/sessions. + runDeferredRouteOpener(); + + // The shell is already usable at this point; session hydration is + // sidebar-local and settles on its own schedule. + settleSessionHydration(() => sessionModule.loadSessions()); } else { console.error('Session module not loaded!'); + // Nothing will hydrate. Settle immediately so the sidebar exposes the + // failure; session-dependent routes must remain unopened without data. + settleSessionHydration(null); } const runNonCriticalStartup = (fn, delay = 4000) => { diff --git a/static/index.html b/static/index.html index fea4e20ac..c73d9a890 100644 --- a/static/index.html +++ b/static/index.html @@ -248,8 +248,8 @@ }, { once: true }); })(); - - + + @@ -286,7 +286,13 @@ if(!document.getElementById('app-loader')){clearInterval(iv);return} render(); },150); - setTimeout(function(){var l=document.getElementById('app-loader');if(l){l.style.opacity='0';setTimeout(function(){l.remove()},300)}},5000); + // startupShell.js hides the loader as soon as the shell is wired; it calls + // back here to stop the wave because this interval is owned by this script. + window.__odysseusLoaderWaveStop=function(){clearInterval(iv)}; + // Last-resort fallback for a boot that never reaches app.js at all. Must + // still REMOVE the node: sessions.js reads its presence as "startup in + // progress" and stops clearing the composer while it is around. + setTimeout(function(){var l=document.getElementById('app-loader');if(l){clearInterval(iv);l.style.opacity='0';setTimeout(function(){l.remove()},300)}},5000); })(); @@ -813,7 +819,13 @@ -
+
+ +
+ Loading chats… +
+