Skip to content

Commit 0404639

Browse files
committed
fix(web): recover deleted routes from shell events
1 parent 1a5f2e3 commit 0404639

1 file changed

Lines changed: 25 additions & 2 deletions

File tree

apps/web/src/routes/__root.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import { Throttler } from "@tanstack/react-pacer";
2525

2626
import { APP_DISPLAY_NAME, APP_VERSION } from "../branding";
2727
import { DesktopWindowControls } from "../components/DesktopWindowControls";
28-
import { readSidebarUiState } from "../components/Sidebar.uiState";
28+
import {
29+
clearLastThreadRouteIfMatches,
30+
readSidebarUiState,
31+
} from "../components/Sidebar.uiState";
2932
import { RunningChatsQuitCoordinator } from "../components/RunningChatsQuitCoordinator";
3033
import { AppSnapCoordinator } from "../components/AppSnapCoordinator";
3134
import { 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

Comments
 (0)