@@ -25,7 +25,7 @@ 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 { clearLastThreadRouteIfMatches , readSidebarUiState } from "../components/Sidebar.uiState" ;
2929import { RunningChatsQuitCoordinator } from "../components/RunningChatsQuitCoordinator" ;
3030import { AppSnapCoordinator } from "../components/AppSnapCoordinator" ;
3131import { AppSnapWelcomeDialog } from "../components/AppSnapWelcomeDialog" ;
@@ -1130,6 +1130,12 @@ function EventRouter() {
11301130 ? subscribedThreadIdsRef . current
11311131 : nextSubscribedThreadIds ;
11321132 const pathnameRef = useRef ( pathname ) ;
1133+ // `pathnameRef` is refreshed in a passive effect, so the first welcome can
1134+ // otherwise observe the router's provisional root location before a deep
1135+ // link commits. Capture the committed launch path in layout so welcome
1136+ // bootstrap remains a root-only fallback.
1137+ const initialPathnameRef = useRef < string | null > ( null ) ;
1138+ const routeThreadIdRef = useRef ( routeThreadId ) ;
11331139 const handledBootstrapThreadIdRef = useRef < string | null > ( null ) ;
11341140 const bootstrapNavigationSettledRef = useRef ( false ) ;
11351141 const visibleThreadIdsRef = useRef ( subscribedThreadIds ) ;
@@ -1141,8 +1147,15 @@ function EventRouter() {
11411147 // callbacks (welcome handler, scoped-subscription reconcile, terminal cleanup).
11421148 // The refs are seeded via useRef init, so mount reads stay correct before this
11431149 // runs; subsequent renders refresh them here instead of during render.
1150+ useLayoutEffect ( ( ) => {
1151+ if ( initialPathnameRef . current === null ) {
1152+ initialPathnameRef . current = pathname ;
1153+ }
1154+ } , [ pathname ] ) ;
1155+
11441156 useEffect ( ( ) => {
11451157 pathnameRef . current = pathname ;
1158+ routeThreadIdRef . current = routeThreadId ;
11461159 visibleThreadIdsRef . current = subscribedThreadIds ;
11471160 subscribedThreadIdsRef . current = subscribedThreadIds ;
11481161 // Retention must know what is on screen: an evicted visible thread keeps its
@@ -1888,6 +1901,13 @@ function EventRouter() {
18881901 }
18891902 shellSnapshotSequence = item . sequence ;
18901903 applyShellEvent ( item ) ;
1904+ if ( item . kind === "thread-removed" && routeThreadIdRef . current === item . threadId ) {
1905+ // The removal event is authoritative. Leave the stale route now instead
1906+ // of waiting for its empty-snapshot guard, which otherwise leaves a
1907+ // window for launch bootstrap to reclaim the deleted thread path.
1908+ clearLastThreadRouteIfMatches ( item . threadId ) ;
1909+ void navigate ( { to : "/" , replace : true } ) ;
1910+ }
18911911 if ( item . kind === "thread-upserted" ) {
18921912 reconcilePromotedDraftsFromShellThreads ( [ item . thread ] ) ;
18931913 }
@@ -2100,7 +2120,7 @@ function EventRouter() {
21002120 // own recovery: if a missing/deleted route falls back to "/" while the
21012121 // welcome handler is still loading, a late bootstrap must not replace
21022122 // that recovery with a different remembered server thread.
2103- const welcomeArrivedOnRootRoute = pathnameRef . current === "/" ;
2123+ const welcomeArrivedOnRootRoute = initialPathnameRef . current === "/" ;
21042124 // A stored route (including one waiting for empty-snapshot recovery)
21052125 // is resolved by the index route. Bootstrap navigation is only the
21062126 // no-preference root fallback, otherwise it races that resolver and
0 commit comments