@@ -25,7 +25,10 @@ import { Throttler } from "@tanstack/react-pacer";
2525
2626import { APP_DISPLAY_NAME , APP_VERSION } from "../branding" ;
2727import { DesktopWindowControls } from "../components/DesktopWindowControls" ;
28- import { readSidebarUiState } from "../components/Sidebar.uiState" ;
28+ import {
29+ clearLastThreadRouteIfMatches ,
30+ readSidebarUiState ,
31+ } from "../components/Sidebar.uiState" ;
2932import { RunningChatsQuitCoordinator } from "../components/RunningChatsQuitCoordinator" ;
3033import { AppSnapCoordinator } from "../components/AppSnapCoordinator" ;
3134import { AppSnapWelcomeDialog } from "../components/AppSnapWelcomeDialog" ;
@@ -1130,6 +1133,12 @@ function EventRouter() {
11301133 ? subscribedThreadIdsRef . current
11311134 : nextSubscribedThreadIds ;
11321135 const pathnameRef = useRef ( pathname ) ;
1136+ // `pathnameRef` is refreshed in a passive effect, so the first welcome can
1137+ // otherwise observe the router's provisional root location before a deep
1138+ // link commits. Capture the committed launch path in layout so welcome
1139+ // bootstrap remains a root-only fallback.
1140+ const initialPathnameRef = useRef < string | null > ( null ) ;
1141+ const routeThreadIdRef = useRef ( routeThreadId ) ;
11331142 const handledBootstrapThreadIdRef = useRef < string | null > ( null ) ;
11341143 const bootstrapNavigationSettledRef = useRef ( false ) ;
11351144 const visibleThreadIdsRef = useRef ( subscribedThreadIds ) ;
@@ -1141,8 +1150,15 @@ function EventRouter() {
11411150 // callbacks (welcome handler, scoped-subscription reconcile, terminal cleanup).
11421151 // The refs are seeded via useRef init, so mount reads stay correct before this
11431152 // runs; subsequent renders refresh them here instead of during render.
1153+ useLayoutEffect ( ( ) => {
1154+ if ( initialPathnameRef . current === null ) {
1155+ initialPathnameRef . current = pathname ;
1156+ }
1157+ } , [ pathname ] ) ;
1158+
11441159 useEffect ( ( ) => {
11451160 pathnameRef . current = pathname ;
1161+ routeThreadIdRef . current = routeThreadId ;
11461162 visibleThreadIdsRef . current = subscribedThreadIds ;
11471163 subscribedThreadIdsRef . current = subscribedThreadIds ;
11481164 // Retention must know what is on screen: an evicted visible thread keeps its
@@ -1888,6 +1904,13 @@ function EventRouter() {
18881904 }
18891905 shellSnapshotSequence = item . sequence ;
18901906 applyShellEvent ( item ) ;
1907+ if ( item . kind === "thread-removed" && routeThreadIdRef . current === item . threadId ) {
1908+ // The removal event is authoritative. Leave the stale route now instead
1909+ // of waiting for its empty-snapshot guard, which otherwise leaves a
1910+ // window for launch bootstrap to reclaim the deleted thread path.
1911+ clearLastThreadRouteIfMatches ( item . threadId ) ;
1912+ void navigate ( { to : "/" , replace : true } ) ;
1913+ }
18911914 if ( item . kind === "thread-upserted" ) {
18921915 reconcilePromotedDraftsFromShellThreads ( [ item . thread ] ) ;
18931916 }
@@ -2100,7 +2123,7 @@ function EventRouter() {
21002123 // own recovery: if a missing/deleted route falls back to "/" while the
21012124 // welcome handler is still loading, a late bootstrap must not replace
21022125 // that recovery with a different remembered server thread.
2103- const welcomeArrivedOnRootRoute = pathnameRef . current === "/" ;
2126+ const welcomeArrivedOnRootRoute = initialPathnameRef . current === "/" ;
21042127 // A stored route (including one waiting for empty-snapshot recovery)
21052128 // is resolved by the index route. Bootstrap navigation is only the
21062129 // no-preference root fallback, otherwise it races that resolver and
0 commit comments