| import { |
| Links, |
| LinksFunction, |
| Meta, |
| MetaFunction, |
| Outlet, |
| Scripts, |
| ScrollRestoration, |
| useLocation, |
| useNavigate, |
| useNavigation as useRouterNavigation, |
| } from "react-router"; |
| import "./tailwind.css"; |
| import "./index.css"; |
| import React from "react"; |
| import { useQuery, useQueryClient } from "@tanstack/react-query"; |
| import { Toaster } from "react-hot-toast"; |
| import { |
| clearCachedAgentServerInfo, |
| isAgentServerUnavailableError, |
| isAgentServerAuthError, |
| } from "#/api/agent-server-compatibility"; |
| import { |
| getLockedCloudAuthMode, |
| getLockedCloudHost, |
| isAuthRequiredAndMissing, |
| isSameCloudHost, |
| } from "#/api/agent-server-config"; |
| import { |
| authenticateWithMainAppCookie, |
| redirectToMainAppLogin, |
| shouldUseMainAppCookieAuth, |
| } from "#/api/main-app-auth"; |
| import { getEffectiveLocalBackend } from "#/api/backend-registry/active-store"; |
| import { useActiveBackendContext } from "#/contexts/active-backend-context"; |
| import { |
| isCloudBackendApiKeyOrNetworkHealthError, |
| isCloudBackendLoggedOutHealthError, |
| useBackendsHealth, |
| } from "#/hooks/query/use-backends-health"; |
| import { TOAST_OPTIONS } from "#/utils/custom-toast-handlers"; |
| import { LoadingSpinner } from "#/components/shared/loading-spinner"; |
| import { useConfig } from "#/hooks/query/use-config"; |
| import { QUERY_KEYS } from "#/hooks/query/query-keys"; |
| import { AgentServerUIRoot } from "#/components/providers"; |
| import { TelemetryConsentBanner } from "#/components/features/analytics/telemetry-consent-banner"; |
| import { buildAgentCanvasPath } from "#/utils/base-path"; |
| import { useOnboardingCompletion } from "#/components/features/onboarding/use-onboarding-completion"; |
| import { NavigationProvider } from "#/context/navigation-context"; |
| import { |
| applyColorTheme, |
| readPersistedColorTheme, |
| } from "#/themes/color-themes"; |
|
|
| |
| function ColorThemeApplier() { |
| React.useEffect(() => { |
| applyColorTheme(readPersistedColorTheme()); |
| }, []); |
| return null; |
| } |
|
|
| |
| |
| const ManageBackendsModal = React.lazy(() => |
| import("#/components/features/backends/manage-backends-modal").then((m) => ({ |
| default: m.ManageBackendsModal, |
| })), |
| ); |
|
|
| |
| const ApiKeyEntryScreen = React.lazy( |
| () => import("#/components/features/backends/api-key-entry-screen"), |
| ); |
|
|
| |
| |
| const OnboardingModal = React.lazy(() => |
| import("#/components/features/onboarding/onboarding-modal").then((m) => ({ |
| default: m.OnboardingModal, |
| })), |
| ); |
|
|
| |
| |
| const BackendFormModal = React.lazy(() => |
| import("#/components/features/backends/backend-form-modal").then((m) => ({ |
| default: m.BackendFormModal, |
| })), |
| ); |
|
|
| export function Layout({ children }: { children: React.ReactNode }) { |
| return ( |
| <html lang="en"> |
| <head> |
| <meta charSet="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <Meta /> |
| <Links /> |
| </head> |
| <body data-agent-server-ui="" className="m-0"> |
| <AgentServerUIRoot contentClassName="min-h-screen"> |
| <ColorThemeApplier /> |
| {children} |
| <Toaster toastOptions={TOAST_OPTIONS} /> |
| <div id="modal-portal-exit" /> |
| </AgentServerUIRoot> |
| <ScrollRestoration /> |
| <Scripts /> |
| </body> |
| </html> |
| ); |
| } |
|
|
| function AgentServerBootstrapLoading() { |
| return ( |
| <main className="min-h-screen bg-base px-6 py-10 text-white"> |
| <div className="mx-auto flex min-h-screen max-w-6xl items-center justify-center"> |
| <div className="rounded-3xl border border-white/10 bg-base/80 px-8 py-10 shadow-2xl"> |
| <LoadingSpinner size="large" /> |
| </div> |
| </div> |
| </main> |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function MissingAgentServerScreen() { |
| const queryClient = useQueryClient(); |
|
|
| |
| |
| |
| |
| const handleClose = React.useCallback(() => { |
| if (getEffectiveLocalBackend()) { |
| clearCachedAgentServerInfo(); |
| void queryClient.invalidateQueries({ |
| queryKey: QUERY_KEYS.WEB_CLIENT_CONFIG, |
| }); |
| } |
| }, [queryClient]); |
|
|
| return ( |
| <main |
| data-testid="agent-server-onboarding-screen" |
| className="min-h-screen bg-base" |
| > |
| <React.Suspense fallback={null}> |
| <ManageBackendsModal onClose={handleClose} recoveryMode /> |
| </React.Suspense> |
| </main> |
| ); |
| } |
| function FirstRunOnboardingScreen({ onClose }: { onClose: () => void }) { |
| const location = useLocation(); |
| const navigate = useNavigate(); |
| const routerNavigation = useRouterNavigation(); |
| const conversationId = |
| location.pathname.match(/^\/conversations\/([^/]+)/)?.[1] ?? null; |
| const navigationValue = React.useMemo( |
| () => ({ |
| currentPath: location.pathname, |
| conversationId, |
| isNavigating: Boolean(routerNavigation.location), |
| navigate: (to: string, options?: { replace?: boolean }) => |
| navigate(to, options), |
| }), |
| [conversationId, location.pathname, navigate, routerNavigation.location], |
| ); |
|
|
| const lockedCloudHost = getLockedCloudHost(); |
| const isLockedToCloud = lockedCloudHost !== null; |
|
|
| |
| |
| |
| |
| if (isLockedToCloud) { |
| return ( |
| <main |
| data-testid="first-run-onboarding-screen" |
| className="min-h-screen bg-base" |
| > |
| <React.Suspense fallback={<AgentServerBootstrapLoading />}> |
| <BackendFormModal |
| mode="add" |
| onClose={onClose} |
| source="manage_backends_modal" |
| hideCloseButton |
| /> |
| </React.Suspense> |
| </main> |
| ); |
| } |
|
|
| return ( |
| <main |
| data-testid="first-run-onboarding-screen" |
| className="min-h-screen bg-base" |
| > |
| <NavigationProvider value={navigationValue}> |
| <React.Suspense fallback={<AgentServerBootstrapLoading />}> |
| <OnboardingModal onClose={onClose} /> |
| </React.Suspense> |
| </NavigationProvider> |
| </main> |
| ); |
| } |
|
|
| export const links: LinksFunction = () => [ |
| { |
| rel: "icon", |
| type: "image/svg+xml", |
| href: buildAgentCanvasPath("/favicon.svg"), |
| }, |
| ]; |
|
|
| export const meta: MetaFunction = () => [ |
| { title: "OpenHands" }, |
| { name: "description", content: "Let's Start Building!" }, |
| ]; |
|
|
| export default function App() { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const bakedKeyMissing = isAuthRequiredAndMissing(); |
| const hasRegisteredKey = Boolean(getEffectiveLocalBackend()?.apiKey); |
| const authMissing = bakedKeyMissing && !hasRegisteredKey; |
| const { active } = useActiveBackendContext(); |
| const queryClient = useQueryClient(); |
| |
| |
| |
| |
| |
| |
| const lockedCloudHost = getLockedCloudHost(); |
| const lockedCloudAuthMode = getLockedCloudAuthMode(); |
| const isLockedToCloud = lockedCloudHost !== null; |
| |
| |
| |
| |
| |
| const isActiveLockedCloudBackend = |
| isLockedToCloud && |
| active.backend.kind === "cloud" && |
| isSameCloudHost(active.backend.host, lockedCloudHost); |
| const { isCompleted: onboardingCompleted, markCompleted } = |
| useOnboardingCompletion(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const shouldCheckMainAppAuth = shouldUseMainAppCookieAuth(); |
| const showFirstRunOnboarding = isLockedToCloud |
| ? !shouldCheckMainAppAuth && |
| (!isActiveLockedCloudBackend || |
| (lockedCloudAuthMode !== "cookie" && !onboardingCompleted)) |
| : !onboardingCompleted; |
| const mainAppAuth = useQuery({ |
| queryKey: QUERY_KEYS.MAIN_APP_COOKIE_AUTH, |
| queryFn: authenticateWithMainAppCookie, |
| enabled: shouldCheckMainAppAuth && !showFirstRunOnboarding, |
| retry: false, |
| staleTime: 1000 * 60 * 5, |
| meta: { disableToast: true }, |
| }); |
| const waitingForMainAppAuth = |
| shouldCheckMainAppAuth && |
| !showFirstRunOnboarding && |
| mainAppAuth.isPending && |
| !mainAppAuth.isError; |
| const redirectingToMainAppLogin = |
| shouldCheckMainAppAuth && mainAppAuth.data === false; |
| const mainAppAuthAllowsBackendQueries = |
| !shouldCheckMainAppAuth || mainAppAuth.data === true || mainAppAuth.isError; |
|
|
| React.useEffect(() => { |
| if (redirectingToMainAppLogin) redirectToMainAppLogin(); |
| }, [redirectingToMainAppLogin]); |
|
|
| |
| |
| |
| |
| const config = useConfig({ |
| enabled: |
| !authMissing && |
| !showFirstRunOnboarding && |
| mainAppAuthAllowsBackendQueries, |
| }); |
| const activeCloudHealth = useBackendsHealth( |
| active.backend.kind === "cloud" && mainAppAuthAllowsBackendQueries |
| ? [active.backend] |
| : [], |
| )[active.backend.id]; |
| const activeCloudLoggedOut = |
| active.backend.kind === "cloud" && |
| activeCloudHealth?.isConnected === false && |
| isCloudBackendLoggedOutHealthError(activeCloudHealth.lastError); |
| |
| |
| |
| |
| |
| const cookieSessionMaybeExpired = |
| shouldCheckMainAppAuth && activeCloudLoggedOut; |
| React.useEffect(() => { |
| if (!cookieSessionMaybeExpired) return; |
| void queryClient.invalidateQueries({ |
| queryKey: QUERY_KEYS.MAIN_APP_COOKIE_AUTH, |
| }); |
| }, [cookieSessionMaybeExpired, queryClient]); |
| |
| |
| |
| |
| |
| const activeCloudUnreachable = |
| active.backend.kind === "cloud" && |
| activeCloudHealth?.disabled === true && |
| isCloudBackendApiKeyOrNetworkHealthError(activeCloudHealth.lastError); |
|
|
| if (showFirstRunOnboarding) { |
| return ( |
| <> |
| <FirstRunOnboardingScreen onClose={markCompleted} /> |
| <TelemetryConsentBanner /> |
| </> |
| ); |
| } |
|
|
| if ( |
| waitingForMainAppAuth || |
| redirectingToMainAppLogin || |
| (cookieSessionMaybeExpired && mainAppAuth.isFetching) |
| ) { |
| return <AgentServerBootstrapLoading />; |
| } |
|
|
| |
| |
| if (authMissing || isAgentServerAuthError(config.error)) { |
| return ( |
| <React.Suspense fallback={<AgentServerBootstrapLoading />}> |
| <ApiKeyEntryScreen /> |
| </React.Suspense> |
| ); |
| } |
|
|
| if (config.isPending || config.isLoading) { |
| return <AgentServerBootstrapLoading />; |
| } |
|
|
| if ( |
| activeCloudLoggedOut || |
| activeCloudUnreachable || |
| isAgentServerUnavailableError(config.error) |
| ) { |
| return <MissingAgentServerScreen />; |
| } |
|
|
| return ( |
| <> |
| <Outlet /> |
| <TelemetryConsentBanner /> |
| </> |
| ); |
| } |
|
|