Skip to content

Commit c92170d

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

1 file changed

Lines changed: 22 additions & 2 deletions

File tree

apps/web/src/routes/__root.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ 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 { clearLastThreadRouteIfMatches, readSidebarUiState } from "../components/Sidebar.uiState";
2929
import { RunningChatsQuitCoordinator } from "../components/RunningChatsQuitCoordinator";
3030
import { AppSnapCoordinator } from "../components/AppSnapCoordinator";
3131
import { 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

Comments
 (0)