diff --git a/ui/web/src/components/layout/app-layout.tsx b/ui/web/src/components/layout/app-layout.tsx
index 07437c24..6a144484 100644
--- a/ui/web/src/components/layout/app-layout.tsx
+++ b/ui/web/src/components/layout/app-layout.tsx
@@ -1,8 +1,9 @@
-import { Outlet } from "react-router";
+import { Outlet, useLocation } from "react-router";
import { WifiOff } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Sidebar } from "./sidebar";
import { Topbar } from "./topbar";
+import { ErrorBoundary } from "@/components/shared/error-boundary";
import { useUiStore } from "@/stores/use-ui-store";
import { useAuthStore } from "@/stores/use-auth-store";
import { useIsMobile } from "@/hooks/use-media-query";
@@ -10,6 +11,7 @@ import { cn } from "@/lib/utils";
export function AppLayout() {
const { t } = useTranslation("common");
+ const location = useLocation();
const sidebarCollapsed = useUiStore((s) => s.sidebarCollapsed);
const mobileSidebarOpen = useUiStore((s) => s.mobileSidebarOpen);
const setMobileSidebarOpen = useUiStore((s) => s.setMobileSidebarOpen);
@@ -49,7 +51,9 @@ export function AppLayout() {
)}
-
+
+
+
diff --git a/ui/web/src/hooks/use-query-invalidation.ts b/ui/web/src/hooks/use-query-invalidation.ts
index c19f52db..2bccc64b 100644
--- a/ui/web/src/hooks/use-query-invalidation.ts
+++ b/ui/web/src/hooks/use-query-invalidation.ts
@@ -1,6 +1,7 @@
-import { useCallback } from "react";
+import { useCallback, useEffect, useRef } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useWsEvent } from "./use-ws-event";
+import { useAuthStore } from "@/stores/use-auth-store";
import { Events } from "@/api/protocol";
import { queryKeys } from "@/lib/query-keys";
import type { AgentEventPayload } from "@/types/chat";
@@ -61,6 +62,21 @@ export function useWsQueryInvalidation() {
[queryClient],
);
+ // Invalidate all queries on WS reconnect so pages get fresh data
+ const connected = useAuthStore((s) => s.connected);
+ const wasConnectedRef = useRef(false);
+ const hasConnectedOnceRef = useRef(false);
+
+ useEffect(() => {
+ if (connected && !wasConnectedRef.current && hasConnectedOnceRef.current) {
+ queryClient.invalidateQueries();
+ }
+ if (connected) {
+ hasConnectedOnceRef.current = true;
+ }
+ wasConnectedRef.current = connected;
+ }, [connected, queryClient]);
+
useWsEvent(Events.AGENT, handleAgentEvent);
useWsEvent(Events.TRACE_UPDATED, handleTraceUpdated);
useWsEvent(Events.CRON, handleCronEvent);
diff --git a/ui/web/src/lib/lazy-with-retry.ts b/ui/web/src/lib/lazy-with-retry.ts
new file mode 100644
index 00000000..a1a3be39
--- /dev/null
+++ b/ui/web/src/lib/lazy-with-retry.ts
@@ -0,0 +1,31 @@
+import { lazy, type ComponentType } from "react";
+
+/**
+ * Wraps React.lazy() with retry logic for failed chunk loads.
+ * - Retries the import once on failure (handles transient network errors)
+ * - If retry fails, forces a page reload to clear stale chunk URLs (e.g. after a deploy)
+ * - sessionStorage guard prevents infinite reload loops
+ */
+export function lazyWithRetry>(
+ importFn: () => Promise<{ default: T }>,
+) {
+ return lazy(async () => {
+ try {
+ return await importFn();
+ } catch {
+ // Retry once after transient failure
+ try {
+ return await importFn();
+ } catch (retryError) {
+ // Force reload to clear stale chunks — but only once per session
+ const alreadyReloaded = sessionStorage.getItem("chunk-failed-reload");
+ if (!alreadyReloaded) {
+ sessionStorage.setItem("chunk-failed-reload", "1");
+ window.location.reload();
+ }
+ // Already reloaded and still failing — let ErrorBoundary handle it
+ throw retryError;
+ }
+ }
+ });
+}
diff --git a/ui/web/src/routes.tsx b/ui/web/src/routes.tsx
index 741469c3..46c2cb4b 100644
--- a/ui/web/src/routes.tsx
+++ b/ui/web/src/routes.tsx
@@ -1,96 +1,98 @@
-import { lazy, Suspense } from "react";
+import { Suspense } from "react";
import { Routes, Route, Navigate } from "react-router";
import { AppLayout } from "@/components/layout/app-layout";
import { RequireAuth } from "@/components/shared/require-auth";
import { RequireSetup } from "@/components/shared/require-setup";
+import { ErrorBoundary } from "@/components/shared/error-boundary";
import { ROUTES } from "@/lib/constants";
+import { lazyWithRetry } from "@/lib/lazy-with-retry";
// Lazy-loaded pages
-const LoginPage = lazy(() =>
+const LoginPage = lazyWithRetry(() =>
import("@/pages/login/login-page").then((m) => ({ default: m.LoginPage })),
);
-const OverviewPage = lazy(() =>
+const OverviewPage = lazyWithRetry(() =>
import("@/pages/overview/overview-page").then((m) => ({ default: m.OverviewPage })),
);
-const ChatPage = lazy(() =>
+const ChatPage = lazyWithRetry(() =>
import("@/pages/chat/chat-page").then((m) => ({ default: m.ChatPage })),
);
-const AgentsPage = lazy(() =>
+const AgentsPage = lazyWithRetry(() =>
import("@/pages/agents/agents-page").then((m) => ({ default: m.AgentsPage })),
);
-const SessionsPage = lazy(() =>
+const SessionsPage = lazyWithRetry(() =>
import("@/pages/sessions/sessions-page").then((m) => ({ default: m.SessionsPage })),
);
-const SkillsPage = lazy(() =>
+const SkillsPage = lazyWithRetry(() =>
import("@/pages/skills/skills-page").then((m) => ({ default: m.SkillsPage })),
);
-const CronPage = lazy(() =>
+const CronPage = lazyWithRetry(() =>
import("@/pages/cron/cron-page").then((m) => ({ default: m.CronPage })),
);
-const ConfigPage = lazy(() =>
+const ConfigPage = lazyWithRetry(() =>
import("@/pages/config/config-page").then((m) => ({ default: m.ConfigPage })),
);
-const TracesPage = lazy(() =>
+const TracesPage = lazyWithRetry(() =>
import("@/pages/traces/traces-page").then((m) => ({ default: m.TracesPage })),
);
-const ChannelsPage = lazy(() =>
+const ChannelsPage = lazyWithRetry(() =>
import("@/pages/channels/channels-page").then((m) => ({ default: m.ChannelsPage })),
);
-const ApprovalsPage = lazy(() =>
+const ApprovalsPage = lazyWithRetry(() =>
import("@/pages/approvals/approvals-page").then((m) => ({ default: m.ApprovalsPage })),
);
-const NodesPage = lazy(() =>
+const NodesPage = lazyWithRetry(() =>
import("@/pages/nodes/nodes-page").then((m) => ({ default: m.NodesPage })),
);
-const LogsPage = lazy(() =>
+const LogsPage = lazyWithRetry(() =>
import("@/pages/logs/logs-page").then((m) => ({ default: m.LogsPage })),
);
-const ProvidersPage = lazy(() =>
+const ProvidersPage = lazyWithRetry(() =>
import("@/pages/providers/providers-page").then((m) => ({ default: m.ProvidersPage })),
);
-const CustomToolsPage = lazy(() =>
+const CustomToolsPage = lazyWithRetry(() =>
import("@/pages/custom-tools/custom-tools-page").then((m) => ({ default: m.CustomToolsPage })),
);
-const MCPPage = lazy(() =>
+const MCPPage = lazyWithRetry(() =>
import("@/pages/mcp/mcp-page").then((m) => ({ default: m.MCPPage })),
);
-const TeamsPage = lazy(() =>
+const TeamsPage = lazyWithRetry(() =>
import("@/pages/teams/teams-page").then((m) => ({ default: m.TeamsPage })),
);
-const BuiltinToolsPage = lazy(() =>
+const BuiltinToolsPage = lazyWithRetry(() =>
import("@/pages/builtin-tools/builtin-tools-page").then((m) => ({ default: m.BuiltinToolsPage })),
);
-const TtsPage = lazy(() =>
+const TtsPage = lazyWithRetry(() =>
import("@/pages/tts/tts-page").then((m) => ({ default: m.TtsPage })),
);
-const EventsPage = lazy(() =>
+const EventsPage = lazyWithRetry(() =>
import("@/pages/events/events-page").then((m) => ({ default: m.EventsPage })),
);
-const StoragePage = lazy(() =>
+const StoragePage = lazyWithRetry(() =>
import("@/pages/storage/storage-page").then((m) => ({ default: m.StoragePage })),
);
-const SetupPage = lazy(() =>
+const SetupPage = lazyWithRetry(() =>
import("@/pages/setup/setup-page").then((m) => ({ default: m.SetupPage })),
);
-const PendingMessagesPage = lazy(() =>
+const PendingMessagesPage = lazyWithRetry(() =>
import("@/pages/pending-messages/pending-messages-page").then((m) => ({ default: m.PendingMessagesPage })),
);
-const MemoryPage = lazy(() =>
+const MemoryPage = lazyWithRetry(() =>
import("@/pages/memory/memory-page").then((m) => ({ default: m.MemoryPage })),
);
-const KnowledgeGraphPage = lazy(() =>
+const KnowledgeGraphPage = lazyWithRetry(() =>
import("@/pages/knowledge-graph/knowledge-graph-page").then((m) => ({ default: m.KnowledgeGraphPage })),
);
-const ContactsPage = lazy(() =>
+const ContactsPage = lazyWithRetry(() =>
import("@/pages/contacts/contacts-page").then((m) => ({ default: m.ContactsPage })),
);
-const ActivityPage = lazy(() =>
+const ActivityPage = lazyWithRetry(() =>
import("@/pages/activity/activity-page").then((m) => ({ default: m.ActivityPage })),
);
-const CliCredentialsPage = lazy(() =>
+const CliCredentialsPage = lazyWithRetry(() =>
import("@/pages/cli-credentials/cli-credentials-page").then((m) => ({ default: m.CliCredentialsPage })),
);
-const ApiKeysPage = lazy(() =>
+const ApiKeysPage = lazyWithRetry(() =>
import("@/pages/api-keys/api-keys-page").then((m) => ({ default: m.ApiKeysPage })),
);
@@ -104,6 +106,7 @@ function PageLoader() {
export function AppRoutes() {
return (
+
}>
} />
@@ -171,5 +174,6 @@ export function AppRoutes() {
} />
+
);
}