import React from 'react';
import { Routes, Route, Navigate, useLocation, useMatch, useParams } from 'react-router-dom';
import V2Layout from './components/V2Layout';
import V2FeaturePage from './components/V2FeaturePage';
import { useAuth } from '../context/AuthContext';
import { useTimeZoneCapture } from './hooks/useTimeZoneCapture';
import RegistrationInviteRequired from '../components/RegistrationInviteRequired';
import VerifyEmail from '../components/VerifyEmail';
import DiscordCallback from '../components/DiscordCallback';
import V2EmailVerificationBanner from './components/V2EmailVerificationBanner';
import ProtectedRoute from '../components/ProtectedRoute';
import '@fontsource-variable/bricolage-grotesque';
import '@fontsource/ibm-plex-sans/400.css';
import '@fontsource/ibm-plex-sans/500.css';
import '@fontsource/ibm-plex-sans/600.css';
import '@fontsource/ibm-plex-mono/500.css';
import './v2.css';
// D223 shared primitives (TASK-730): the keys and segment chat and the sidebar use.
import './primitives/primitives.css';
import { applyV2Theme, getV2Theme } from './utils/theme';
// TASK-145 — inside the shell, only the surfaces the composer path needs stay
// in this chunk (V2Layout, the auth gate, the feature frame). Everything else
// is a route of its own: the chat first screen used to wait for the board, the
// connectors catalogue, every admin page and the artifacts browser because they
// were all one module graph with it. The Suspense boundary in V2App covers the
// whole route table, so a chunk in flight shows the same the auth
// check already shows.
const V2Login = React.lazy(() => import('./components/V2Login'));
// TASK-730 (D223): the shared primitives rendered alone, for frame checks.
const V2PrimitivesPage = React.lazy(() => import('./primitives/V2PrimitivesPage'));
const V2Register = React.lazy(() => import('./components/V2Register'));
const V2OAuthComplete = React.lazy(() => import('./components/V2OAuthComplete'));
const V2ForgotPassword = React.lazy(() => import('./components/V2ForgotPassword'));
const V2ResetPassword = React.lazy(() => import('./components/V2ResetPassword'));
const V2YourTeamPage = React.lazy(() => import('./components/V2YourTeamPage'));
const V2InviteRedeem = React.lazy(() => import('./components/V2InviteRedeem'));
const V2CommunityRedirect = React.lazy(() => import('./components/V2CommunityRedirect'));
const V2SettingsPage = React.lazy(() => import('./components/V2SettingsPage'));
const V2AgentProfile = React.lazy(() => import('./agents/V2AgentProfile'));
const V2AgentSettingsPage = React.lazy(() => import('./agents/V2AgentSettingsPage'));
const V2AgentMemory = React.lazy(() => import('./agents/V2AgentMemory'));
const UserProfile = React.lazy(() => import('../components/UserProfile'));
const V2AgentBYO = React.lazy(() => import('./components/V2AgentBYO'));
const V2ConnectPage = React.lazy(() => import('./components/V2ConnectPage'));
const ConnectorsPage = React.lazy(() => import('./connectors/ConnectorsPage'));
const V2PodBoard = React.lazy(() => import('./components/V2PodBoard'));
const V2RunTrace = React.lazy(() => import('./components/V2RunTrace'));
const V2TaskRecord = React.lazy(() => import('./components/V2TaskRecord'));
const V2ActivityPage = React.lazy(() => import('./components/V2ActivityPage'));
const V2ArtifactsPage = React.lazy(() => import('./components/V2ArtifactsPage'));
const ChatRoom = React.lazy(() => import('../components/ChatRoom'));
const ApiDevPage = React.lazy(() => import('../components/ApiDevPage'));
const PodContextDevPage = React.lazy(() => import('../components/PodContextDevPage'));
const GlobalIntegrations = React.lazy(() => import('../components/admin/GlobalIntegrations'));
const V2AdminUsers = React.lazy(() => import('./components/V2AdminUsers'));
const V2AdminAnalytics = React.lazy(() => import('./components/V2AdminAnalytics'));
class V2ErrorBoundary extends React.Component<{ children: React.ReactNode }, { error: Error | null }> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error: Error) {
return { error };
}
componentDidCatch(error: Error) {
console.error('V2 runtime error:', error);
}
render() {
if (this.state.error) {
return (
Something went wrong
{this.state.error.message}
);
}
return this.props.children;
}
}
// This is inline rather than an
so the mark's existing three dots can
// use the product typing rhythm. Its geometry matches the canonical minimal
// mark in frontend/design-system/assets/commonly-mark.svg: one C ring, then
// dots at x=25/32/39 with r=2.4 in a 64px viewBox.
const V2Boot: React.FC = () => (
);
const V2AuthenticatedShell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
);
const V2RequireAuth: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
const location = useLocation();
if (loading) {
return ;
}
if (!isAuthenticated) {
return ;
}
return {children};
};
// One gate owns both `/v2` and `/v2/pods/:podId`, so auto-selection changes
// the URL without remounting the shell. Other deep routes stay behind
// V2RequireAuth (the `*` branch below).
const V2PodShellGate: React.FC = () => {
const { isAuthenticated, loading } = useAuth();
const location = useLocation();
const podRoute = useMatch('/v2/pods/:podId');
if (loading) {
return ;
}
if (!isAuthenticated) {
return location.pathname === '/v2' || location.pathname === '/v2/'
?
: ;
}
// Reject legacy pod-type paths before mounting V2Layout, which otherwise
// remembers the type name as the last selected pod.
if (podRoute?.params.podId && podTypeRoutes.has(podRoute.params.podId)) {
return ;
}
return ;
};
const podTypeRoutes = new Set(['chat', 'study', 'games', 'gaming', 'agent-admin', 'agent-room']);
const V2UseCaseRedirect: React.FC = () => {
const { useCaseId } = useParams<{ useCaseId: string }>();
return ;
};
const feature = (
title: string,
children: React.ReactNode,
showPodsSidebar = false,
showHeader = true,
className = '',
) => (
{children}
);
const V2LegacyChatRedirect: React.FC = () => {
const { podId } = useParams<{ podId: string }>();
return ;
};
const V2App: React.FC = () => {
// The daily digest's 08:00 is the person's own (TASK-257 Phase 2b).
useTimeZoneCapture();
React.useEffect(() => {
applyV2Theme(getV2Theme());
}, []);
React.useEffect(() => {
try {
sessionStorage.setItem('commonly.v2.active', '1');
} catch {
// Ignore browsers that disallow sessionStorage.
}
// Claim the page canvas while v2 is mounted. App.tsx unconditionally
// stamps `modern-ui` (the V1 dark gradient) on , and .v2-root only
// paints its own box — so the V1 darkness showed through on load flash,
// overscroll, and every gap (2026-08-24). The class must out-rank
// body.modern-ui, not merely follow it in bundle order.
document.body.classList.add('v2-canvas');
return () => {
document.body.classList.remove('v2-canvas');
};
}, []);
return (
}>
}>
} />
{/* Public read-only agent profile — the "meet the agent" identity card.
Singular /v2/agent/... so it never collides with the authed plural
/v2/agents (Your Team). */}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
{/* Community access is resolved by the membership-gated pod probe.
Keep this outside V2RequireAuth so an anonymous 401 can fall
through to the public invite preview instead of the login wall. */}
} />
{/* Pod invite redeem — handles its own auth gate (redirects to
/v2/login?next=... when anonymous). Must sit OUTSIDE the
V2RequireAuth wrapper so an anonymous click on the share link
hits the gate cleanly instead of bouncing to / first. */}
} />
{/* Board must precede the param routes: the static "board"
segment outranks both pods/:podId and pods/:podType/:roomId
in v6 matching, but keeping it first makes intent legible. */}
,
false,
/* V2PodBoard owns its own header (pod name + back). */
false,
'v2-feature--board',
)}
/>
,
false,
/* ChatRoom owns its own sticky AppBar with title/tabs;
suppress the v2 feature header so the user does not
get stacked chrome along the top of the route. */
false,
)}
/>
}
/>
,
false,
/* V2YourTeamPage owns its own header — suppress the
generic V2FeaturePage chrome to avoid stacked titles. */
false,
)}
/>
}
/>
}
/>
, false, false)}
/>
, false, false)}
/>
}
/>
,
false,
// The Signal artboard owns this page's display-sized head.
false,
'v2-feature--connectors',
)}
/>
, false, false, 'v2-feature--artifacts')}
/>
, false, false)}
/>
, false, false)}
/>
, false, false, 'v2-feature--activity')}
/>
, false, false, 'v2-feature--settings')}
/>
}
/>
}
/>
)}
/>
,
false,
)}
/>
,
false,
)}
/>
,
false,
)}
/>
,
false,
)}
/>
,
false,
)}
/>
} />
)}
/>
);
};
export default V2App;