From 603728a3fb2a69d7f02fceaa57d033c09265ee6d Mon Sep 17 00:00:00 2001 From: Logan Date: Wed, 19 Aug 2026 18:09:10 -0400 Subject: [PATCH] fix(shell): let the landing page replace the launch entry The app navigates twice on startup and both are deliberate: the eager `navigate -> home` that paints without waiting for the backend, and the configured page `GetDefaultPage()` resolves to a moment later. Only the first replaced the launch entry, so the second stacked on it and a fresh session was already one entry deep before the user had touched anything. The first back press therefore replayed home over home. On desktop that was invisible until this branch drew a Back button, which rendered live at the root and did nothing; on Android `webView.canGoBack()` was true, so the press that should have exited the app was swallowed -- the exact fault the replace-the-launch-entry rule exists to prevent, defeated by there being two launch navigations rather than one. Guarded on still being at index 0 rather than on a flag: that call is asynchronous and the user can navigate while it is in flight, so past the root this is an ordinary navigation and a slow answer cannot overwrite an entry they made. Closes #142 --- frontend/index.ts | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/frontend/index.ts b/frontend/index.ts index bfe19f7..7b51668 100644 --- a/frontend/index.ts +++ b/frontend/index.ts @@ -252,9 +252,30 @@ function publishDepth(): void { } function recordNavigation(detail: { view: string; [key: string]: any }): void { - // `_isBack` is bookkeeping, not destination: keeping it in the entry - // would make a replayed navigation claim to be a back-navigation. - const { _isBack: _ignored, ...nav } = detail; + // `_isBack` and `_replace` are bookkeeping, not destination: keeping + // either in the entry would make a replayed navigation claim to be + // one. + const { _isBack: _ignored, _replace: replace, ...nav } = detail; + + // Still launching: the configured landing page is not a navigation + // *away* from the eager one, it is the same arrival arriving late + // (#142). Pushing it left the app one entry deep before the user + // had touched anything, so the first back press replayed home over + // home -- invisible on desktop until #6 drew a Back button, and on + // Android the press that should have exited the app instead did + // nothing, because `canGoBack()` was true. + // + // Guarded on being at the root rather than on a flag, because + // `GetDefaultPage()` is a backend call and the user can navigate + // while it is in flight: past index 0 this is an ordinary + // navigation, or a slow answer would overwrite an entry they made. + if (historyStarted && replace && currentIndex === 0) { + history.replaceState({ yjNav: nav, yjIdx: 0 }, ''); + maxIndex = 0; + publishDepth(); + + return; + } // Same URL, deliberately: the app has no routes, and a path a // reload cannot resolve is worse than no path at all. @@ -570,14 +591,17 @@ GetDefaultPage() document.dispatchEvent(new CustomEvent('navigate', { bubbles: true, composed: true, - detail: { view: view || 'home' }, + // Part of launching, not a navigation away from the eager + // 'home' above: it replaces that entry rather than + // stacking on it (#142). + detail: { view: view || 'home', _replace: true }, })); }) .catch(() => { document.dispatchEvent(new CustomEvent('navigate', { bubbles: true, composed: true, - detail: { view: 'home' }, + detail: { view: 'home', _replace: true }, })); });