Skip to content
Closed
16 changes: 16 additions & 0 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ import { relayClient } from "@/shared/api/relayClient";
import { useIdentityQuery } from "@/shared/api/hooks";
import { useRelayAutoHeal } from "@/shared/api/useRelayAutoHeal";
import { useDeferredStartup } from "@/shared/hooks/useDeferredStartup";
import { preloadAgentsScreen } from "@/app/routes/agents";
import { preloadChannelRouteScreen } from "@/app/routes/channels.$channelId";
import { preloadChannelViews } from "@/features/channels/ui/ChannelScreenLazyViews";
import { joinChannel } from "@/shared/api/tauri";
import type { Channel, RelayEvent, SearchHit } from "@/shared/api/types";
import { ChannelNavigationProvider } from "@/shared/context/ChannelNavigationContext";
Expand Down Expand Up @@ -567,6 +570,19 @@ export function AppShell() {
};
}, []);

// Warm the lazy route chunks (channel timeline, forum, agents) once the shell
// is idle, so the FIRST main-nav transition doesn't stall on a cold chunk
// fetch+parse. `startupReady` is the existing idle-or-timeout gate; the chunk
// imports dedupe, so racing an actual navigation is harmless.
React.useEffect(() => {
if (!startupReady) {
return;
}
preloadChannelRouteScreen();
preloadChannelViews();
preloadAgentsScreen();
}, [startupReady]);

React.useEffect(() => {
const numericCount =
unreadChannelNotificationCount + homeBadgeCountExcludingHighPriority;
Expand Down
18 changes: 17 additions & 1 deletion desktop/src/app/routes/agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,27 @@ import { createFileRoute } from "@tanstack/react-router";

import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";

// The chunk import is hoisted so it can be triggered eagerly (route preload)
// as well as lazily on render — calling it twice is a no-op, the module loader
// dedupes and caches the in-flight promise.
const importAgentsScreen = () => import("@/features/agents/ui/AgentsScreen");

const AgentsScreen = React.lazy(async () => {
const module = await import("@/features/agents/ui/AgentsScreen");
const module = await importAgentsScreen();
return { default: module.AgentsScreen };
});

// AgentsScreen wraps a SECOND lazy boundary (AgentsView), so warming the route
// chunk alone still leaves AgentsView cold on first navigation. Warm both. The
// dynamic import() keeps AgentsView in its own chunk; the loader dedupes
// against AgentsScreen's own lazy import of the same module.
/** Warms the AgentsScreen route chunk (and its inner AgentsView) so first
* navigation doesn't stall. */
export function preloadAgentsScreen(): void {
void importAgentsScreen();
void import("@/features/agents/ui/AgentsView");
}

export const Route = createFileRoute("/agents")({
component: AgentsRouteComponent,
});
Expand Down
11 changes: 10 additions & 1 deletion desktop/src/app/routes/channels.$channelId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,20 @@ export const Route = createFileRoute("/channels/$channelId")({
component: ChannelRouteComponent,
});

// Hoisted so the chunk can be warmed eagerly (route preload) as well as loaded
// lazily on render; the loader dedupes repeat calls.
const importChannelRouteScreen = () => import("./ChannelRouteScreen");

const ChannelRouteScreen = React.lazy(async () => {
const module = await import("./ChannelRouteScreen");
const module = await importChannelRouteScreen();
return { default: module.ChannelRouteScreen };
});

/** Warms the ChannelRouteScreen chunk so first channel open doesn't stall. */
export function preloadChannelRouteScreen(): void {
void importChannelRouteScreen();
}

function ChannelRouteComponent() {
const { channelId } = Route.useParams();
const search = Route.useSearch();
Expand Down
Loading