diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index a8a3f41a0..a8e945fda 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,29 +1,37 @@
-import { useEffect, useRef, useState } from 'react';
+import { lazy, Suspense, useEffect, useRef, useState } from 'react';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
import i18n from '@/i18n';
import { Projects } from '@/pages/projects';
import { ProjectTasks } from '@/pages/project-tasks';
-import { FullAttemptLogsPage } from '@/pages/full-attempt-logs';
-import ReleaseNotesPage from '@/pages/release-notes';
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout';
import { Footer } from '@/components/layout/Footer';
import { usePostHog } from 'posthog-js/react';
-import type {
- SessionStartedEvent,
- SessionEndedEvent,
- HeartbeatEvent,
-} from '@/types/analytics';
+import type { SessionStartedEvent, SessionEndedEvent, HeartbeatEvent } from '@/types/analytics';
import { usePageTracking } from '@/hooks/usePageTracking';
import { useNamestexerSessionTracking } from '@/hooks/useNamestexerSessionTracking';
-import {
- AgentSettings,
- GeneralSettings,
- McpSettings,
- ProjectSettings,
- SettingsLayout,
-} from '@/pages/settings/';
+// Lazy-loaded pages for route-based code splitting
+const FullAttemptLogsPage = lazy(() =>
+ import('@/pages/full-attempt-logs').then(module => ({ default: module.FullAttemptLogsPage }))
+);
+const ReleaseNotesPage = lazy(() => import('@/pages/release-notes'));
+const SettingsLayout = lazy(() =>
+ import('@/pages/settings/SettingsLayout').then(module => ({ default: module.SettingsLayout }))
+);
+const GeneralSettings = lazy(() =>
+ import('@/pages/settings/GeneralSettings').then(module => ({ default: module.GeneralSettings }))
+);
+const ProjectSettings = lazy(() =>
+ import('@/pages/settings/ProjectSettings').then(module => ({ default: module.ProjectSettings }))
+);
+const AgentSettings = lazy(() =>
+ import('@/pages/settings/AgentSettings').then(module => ({ default: module.AgentSettings }))
+);
+const McpSettings = lazy(() =>
+ import('@/pages/settings/McpSettings').then(module => ({ default: module.McpSettings }))
+);
+
import {
UserSystemProvider,
useUserSystem,
@@ -43,11 +51,20 @@ import NiceModal from '@ebay/nice-modal-react';
import { OnboardingResult } from '@/components/dialogs/global/OnboardingDialog';
import { ClickedElementsProvider } from '@/contexts/ClickedElementsProvider';
import { GenieMasterWidget } from '@/components/genie-widgets/GenieMasterWidget';
-import { SubGenieProvider } from '@/context/SubGenieContext';
+import { SubGenieProvider } from '@/contexts/SubGenieContext';
import { useIsMobile } from '@/components/mobile/MobileLayout';
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
+// Loading fallback for lazy-loaded routes
+function RouteLoadingFallback() {
+ return (
+
+
+
+ );
+}
+
function AppContent() {
const [isGenieOpen, setIsGenieOpen] = useState(false);
const isMobile = useIsMobile();
@@ -55,9 +72,7 @@ function AppContent() {
useUserSystem();
const posthog = usePostHog();
const sessionStartTimeRef = useRef(Date.now());
- const heartbeatIntervalRef = useRef | null>(
- null
- );
+ const heartbeatIntervalRef = useRef(null);
const eventCountRef = useRef(0);
// Track page navigation
@@ -71,8 +86,7 @@ function AppContent() {
if (!posthog || !analyticsUserId) return;
const userOptedIn = config?.analytics_enabled !== false;
- const isNamestexer =
- config?.github?.primary_email?.endsWith('@namastex.ai');
+ const isNamestexer = config?.github?.primary_email?.endsWith('@namastex.ai');
const contactEmailOptIn = config?.contact_email_opt_in === true;
const contactUsernameOptIn = config?.contact_username_opt_in === true;
@@ -121,35 +135,21 @@ function AppContent() {
posthog.opt_out_capturing();
console.log('[Analytics] Analytics disabled by user preference');
}
- }, [
- config?.analytics_enabled,
- config?.contact_email_opt_in,
- config?.contact_username_opt_in,
- config?.github?.primary_email,
- config?.github?.username,
- analyticsUserId,
- posthog,
- ]);
+ }, [config?.analytics_enabled, config?.contact_email_opt_in, config?.contact_username_opt_in, config?.github?.primary_email, config?.github?.username, analyticsUserId, posthog]);
// Session tracking: session_started, session_ended, and heartbeat
useEffect(() => {
- if (!posthog || !analyticsUserId || config?.analytics_enabled === false)
- return;
+ if (!posthog || !analyticsUserId || config?.analytics_enabled === false) return;
// Capture session_started event
const lastSessionTime = localStorage.getItem('last_session_time');
- const totalSessions = parseInt(
- localStorage.getItem('total_sessions') || '0',
- 10
- );
+ const totalSessions = parseInt(localStorage.getItem('total_sessions') || '0', 10);
const now = Date.now();
let daysSinceLastSession: number | null = null;
if (lastSessionTime) {
const lastTime = parseInt(lastSessionTime, 10);
- daysSinceLastSession = Math.floor(
- (now - lastTime) / (1000 * 60 * 60 * 24)
- );
+ daysSinceLastSession = Math.floor((now - lastTime) / (1000 * 60 * 60 * 24));
}
const sessionStartedEvent: SessionStartedEvent = {
@@ -180,9 +180,7 @@ function AppContent() {
clearInterval(heartbeatIntervalRef.current);
}
- const sessionDuration = Math.floor(
- (Date.now() - sessionStartTimeRef.current) / 1000
- );
+ const sessionDuration = Math.floor((Date.now() - sessionStartTimeRef.current) / 1000);
const sessionEndedEvent: SessionEndedEvent = {
session_duration_seconds: sessionDuration,
events_captured_count: eventCountRef.current,
@@ -312,51 +310,97 @@ function AppContent() {
{/* VS Code full-page logs route (outside ResponsiveLayout for minimal UI) */}
}
+ element={
+ }>
+
+
+ }
/>
}>
- } />
- } />
- } />
+ } />
+ } />
+ } />
+ }
+ />
+ }>
+
+
+ }
+ >
+ } />
}
+ path="general"
+ element={
+ }>
+
+
+ }
/>
- }>
- } />
- } />
- } />
- } />
- } />
-
}
+ path="projects"
+ element={
+ }>
+
+
+ }
/>
- } />
}
+ path="agents"
+ element={
+ }>
+
+
+ }
/>
}
+ path="mcp"
+ element={
+ }>
+
+
+ }
/>
-
-
-
- {/* Hide GenieMasterWidget in mobile view - use bottom nav Genie button instead */}
- {!isMobile && (
- setIsGenieOpen(!isGenieOpen)}
- onClose={() => setIsGenieOpen(false)}
- />
- )}
-
-
+ }
+ />
+ }>
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+
+
+ {/* Hide GenieMasterWidget in mobile view - use bottom nav Genie button instead */}
+ {!isMobile && (
+ setIsGenieOpen(!isGenieOpen)}
+ onClose={() => setIsGenieOpen(false)}
+ />
+ )}
+
+
);