perf(ui): stop session loading from blocking shell (#5927)

* 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 <leograndcontact@gmail.com>
This commit is contained in:
RaresKeY
2026-08-10 19:37:21 +01:00
committed by GitHub
co-authored by Léo
parent 96aca52094
commit dbeed4b63f
7 changed files with 957 additions and 25 deletions
+28 -19
View File
@@ -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) => {