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… +
+