diff --git a/frontend/__tests__/routes/_oh.test.tsx b/frontend/__tests__/routes/_oh.test.tsx
index 9dbf3b786f2..a5e106f1fc9 100644
--- a/frontend/__tests__/routes/_oh.test.tsx
+++ b/frontend/__tests__/routes/_oh.test.tsx
@@ -3,7 +3,7 @@ import { createRemixStub } from "@remix-run/testing";
import { screen, waitFor, within } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import userEvent from "@testing-library/user-event";
-import MainApp from "#/routes/_oh";
+import MainApp from "#/routes/_oh/route";
import * as CaptureConsent from "#/utils/handle-capture-consent";
import i18n from "#/i18n";
diff --git a/frontend/src/components/modals/account-settings-modal.tsx b/frontend/src/components/modals/account-settings-form.tsx
similarity index 96%
rename from frontend/src/components/modals/account-settings-modal.tsx
rename to frontend/src/components/modals/account-settings-form.tsx
index 59b25cc0de9..3b41ab4332f 100644
--- a/frontend/src/components/modals/account-settings-modal.tsx
+++ b/frontend/src/components/modals/account-settings-form.tsx
@@ -14,19 +14,19 @@ import { useAuth } from "#/context/auth-context";
import { useUserPrefs } from "#/context/user-prefs-context";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
-interface AccountSettingsModalProps {
+interface AccountSettingsFormProps {
onClose: () => void;
selectedLanguage: string;
gitHubError: boolean;
analyticsConsent: string | null;
}
-function AccountSettingsModal({
+export function AccountSettingsForm({
onClose,
selectedLanguage,
gitHubError,
analyticsConsent,
-}: AccountSettingsModalProps) {
+}: AccountSettingsFormProps) {
const { gitHubToken, setGitHubToken, logout } = useAuth();
const { saveSettings } = useUserPrefs();
const { t } = useTranslation();
@@ -136,5 +136,3 @@ function AccountSettingsModal({
);
}
-
-export default AccountSettingsModal;
diff --git a/frontend/src/routes/_oh.tsx b/frontend/src/routes/_oh.tsx
deleted file mode 100644
index 9a37b8b4d82..00000000000
--- a/frontend/src/routes/_oh.tsx
+++ /dev/null
@@ -1,268 +0,0 @@
-import React from "react";
-import {
- useRouteError,
- isRouteErrorResponse,
- useLocation,
- Outlet,
-} from "@remix-run/react";
-import { useDispatch } from "react-redux";
-import CogTooth from "#/assets/cog-tooth";
-import { SettingsForm } from "#/components/form/settings-form";
-import AccountSettingsModal from "#/components/modals/account-settings-modal";
-import { DangerModal } from "#/components/modals/confirmation-modals/danger-modal";
-import { LoadingSpinner } from "#/components/modals/loading-project";
-import { ModalBackdrop } from "#/components/modals/modal-backdrop";
-import { UserActions } from "#/components/user-actions";
-import i18n from "#/i18n";
-import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
-import NewProjectIcon from "#/icons/new-project.svg?react";
-import DocsIcon from "#/icons/docs.svg?react";
-import { WaitlistModal } from "#/components/waitlist-modal";
-import { AnalyticsConsentFormModal } from "#/components/analytics-consent-form-modal";
-import { setCurrentAgentState } from "#/state/agent-slice";
-import AgentState from "#/types/agent-state";
-import { useConfig } from "#/hooks/query/use-config";
-import { useGitHubUser } from "#/hooks/query/use-github-user";
-import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
-import { useAIConfigOptions } from "#/hooks/query/use-ai-config-options";
-import { useIsAuthed } from "#/hooks/query/use-is-authed";
-import { useAuth } from "#/context/auth-context";
-import { useEndSession } from "#/hooks/use-end-session";
-import { useUserPrefs } from "#/context/user-prefs-context";
-
-export function ErrorBoundary() {
- const error = useRouteError();
-
- if (isRouteErrorResponse(error)) {
- return (
-
-
{error.status}
-
{error.statusText}
-
- {error.data instanceof Object
- ? JSON.stringify(error.data)
- : error.data}
-
-
- );
- }
- if (error instanceof Error) {
- return (
-
-
Uh oh, an error occurred!
-
{error.message}
-
- );
- }
-
- return (
-
-
Uh oh, an unknown error occurred!
-
- );
-}
-
-export default function MainApp() {
- const { token, gitHubToken, clearToken, logout } = useAuth();
- const { settings, settingsAreUpToDate } = useUserPrefs();
-
- const location = useLocation();
- const dispatch = useDispatch();
- const endSession = useEndSession();
-
- // FIXME: Bad practice to use localStorage directly
- const analyticsConsent = localStorage.getItem("analytics-consent");
-
- const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
- React.useState(false);
- const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
- const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] =
- React.useState(false);
- const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(
- !localStorage.getItem("analytics-consent"),
- );
-
- const config = useConfig();
- const user = useGitHubUser();
- const {
- data: isAuthed,
- isFetched,
- isFetching: isFetchingAuth,
- } = useIsAuthed();
- const aiConfigOptions = useAIConfigOptions();
-
- const gitHubAuthUrl = useGitHubAuthUrl({
- gitHubToken,
- appMode: config.data?.APP_MODE || null,
- gitHubClientId: config.data?.GITHUB_CLIENT_ID || null,
- });
-
- React.useEffect(() => {
- if (isFetched && !isAuthed) clearToken();
- }, [isFetched, isAuthed]);
-
- React.useEffect(() => {
- if (settings.LANGUAGE) {
- i18n.changeLanguage(settings.LANGUAGE);
- }
- }, [settings.LANGUAGE]);
-
- React.useEffect(() => {
- // If the github token is invalid, open the account settings modal again
- if (user.isError) {
- setAccountSettingsModalOpen(true);
- }
- }, [user.isError]);
-
- const handleAccountSettingsModalClose = () => {
- // If the user closes the modal without connecting to GitHub,
- // we need to log them out to clear the invalid token from the
- // local storage
- if (user.isError) logout();
- setAccountSettingsModalOpen(false);
- };
-
- const handleEndSession = () => {
- setStartNewProjectModalIsOpen(false);
- dispatch(setCurrentAgentState(AgentState.LOADING));
- endSession();
- };
-
- return (
-
-
-
-
-
-
- {isAuthed && (!settingsAreUpToDate || settingsModalIsOpen) && (
-
setSettingsModalIsOpen(false)}>
-
- {aiConfigOptions.error && (
-
- {aiConfigOptions.error.message}
-
- )}
-
- AI Provider Configuration
-
-
- To continue, connect an OpenAI, Anthropic, or other LLM account
-
-
- Changing settings during an active session will end the session
-
- {aiConfigOptions.isLoading && (
-
-
-
- )}
- {aiConfigOptions.data && (
-
{
- setSettingsModalIsOpen(false);
- }}
- />
- )}
-
-
- )}
- {accountSettingsModalOpen && (
-
-
-
- )}
- {startNewProjectModalIsOpen && (
-
setStartNewProjectModalIsOpen(false)}>
- setStartNewProjectModalIsOpen(false),
- },
- }}
- />
-
- )}
- {!isFetchingAuth && !isAuthed && config.data?.APP_MODE === "saas" && (
-
- )}
- {consentFormIsOpen && (
-
setConsentFormIsOpen(false)}
- />
- )}
-
- );
-}
diff --git a/frontend/src/routes/_oh/buttons/all-hands-logo-button.tsx b/frontend/src/routes/_oh/buttons/all-hands-logo-button.tsx
new file mode 100644
index 00000000000..74ab5f3cee0
--- /dev/null
+++ b/frontend/src/routes/_oh/buttons/all-hands-logo-button.tsx
@@ -0,0 +1,13 @@
+import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
+
+interface AllHandsLogoButtonProps {
+ onClick: () => void;
+}
+
+export function AllHandsLogoButton({ onClick }: AllHandsLogoButtonProps) {
+ return (
+
+ );
+}
diff --git a/frontend/src/routes/_oh/buttons/docs-button.tsx b/frontend/src/routes/_oh/buttons/docs-button.tsx
new file mode 100644
index 00000000000..3b805556e3d
--- /dev/null
+++ b/frontend/src/routes/_oh/buttons/docs-button.tsx
@@ -0,0 +1,15 @@
+import DocsIcon from "#/icons/docs.svg?react";
+
+export function DocsButton() {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/routes/_oh/buttons/exit-project-button.tsx b/frontend/src/routes/_oh/buttons/exit-project-button.tsx
new file mode 100644
index 00000000000..e187fe99d19
--- /dev/null
+++ b/frontend/src/routes/_oh/buttons/exit-project-button.tsx
@@ -0,0 +1,18 @@
+import NewProjectIcon from "#/icons/new-project.svg?react";
+
+interface ExitProjectButtonProps {
+ onClick: () => void;
+}
+
+export function ExitProjectButton({ onClick }: ExitProjectButtonProps) {
+ return (
+
+ );
+}
diff --git a/frontend/src/routes/_oh/buttons/settings-button.tsx b/frontend/src/routes/_oh/buttons/settings-button.tsx
new file mode 100644
index 00000000000..3e739bcb698
--- /dev/null
+++ b/frontend/src/routes/_oh/buttons/settings-button.tsx
@@ -0,0 +1,18 @@
+import CogTooth from "#/assets/cog-tooth";
+
+interface SettingsButtonProps {
+ onClick: () => void;
+}
+
+export function SettingsButton({ onClick }: SettingsButtonProps) {
+ return (
+
+ );
+}
diff --git a/frontend/src/routes/_oh/modals/account-settings-modal.tsx b/frontend/src/routes/_oh/modals/account-settings-modal.tsx
new file mode 100644
index 00000000000..d1568940486
--- /dev/null
+++ b/frontend/src/routes/_oh/modals/account-settings-modal.tsx
@@ -0,0 +1,27 @@
+import { AccountSettingsForm } from "#/components/modals/account-settings-form";
+import { ModalBackdrop } from "#/components/modals/modal-backdrop";
+import { useUserPrefs } from "#/context/user-prefs-context";
+import { useGitHubUser } from "#/hooks/query/use-github-user";
+
+interface AccountSettingsModalProps {
+ onClose: () => void;
+}
+
+export function AccountSettingsModal({ onClose }: AccountSettingsModalProps) {
+ const user = useGitHubUser();
+ const { settings } = useUserPrefs();
+
+ // FIXME: Bad practice to use localStorage directly
+ const analyticsConsent = localStorage.getItem("analytics-consent");
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/routes/_oh/modals/exit-project-confirmation-modal.tsx b/frontend/src/routes/_oh/modals/exit-project-confirmation-modal.tsx
new file mode 100644
index 00000000000..5d425fd4a6a
--- /dev/null
+++ b/frontend/src/routes/_oh/modals/exit-project-confirmation-modal.tsx
@@ -0,0 +1,42 @@
+import { useDispatch } from "react-redux";
+import { DangerModal } from "#/components/modals/confirmation-modals/danger-modal";
+import { ModalBackdrop } from "#/components/modals/modal-backdrop";
+import { useEndSession } from "#/hooks/use-end-session";
+import { setCurrentAgentState } from "#/state/agent-slice";
+import AgentState from "#/types/agent-state";
+
+interface ExitProjectConfirmationModalProps {
+ onClose: () => void;
+}
+
+export function ExitProjectConfirmationModal({
+ onClose,
+}: ExitProjectConfirmationModalProps) {
+ const dispatch = useDispatch();
+ const endSession = useEndSession();
+
+ const handleEndSession = () => {
+ onClose();
+ dispatch(setCurrentAgentState(AgentState.LOADING));
+ endSession();
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/routes/_oh/modals/settings-modal.tsx b/frontend/src/routes/_oh/modals/settings-modal.tsx
new file mode 100644
index 00000000000..d09b73a33eb
--- /dev/null
+++ b/frontend/src/routes/_oh/modals/settings-modal.tsx
@@ -0,0 +1,50 @@
+import { SettingsForm } from "#/components/form/settings-form";
+import { LoadingSpinner } from "#/components/modals/loading-project";
+import { ModalBackdrop } from "#/components/modals/modal-backdrop";
+import { useUserPrefs } from "#/context/user-prefs-context";
+import { useAIConfigOptions } from "#/hooks/query/use-ai-config-options";
+
+interface SettingsModalProps {
+ onClose: () => void;
+}
+
+export function SettingsModal({ onClose }: SettingsModalProps) {
+ const { settings } = useUserPrefs();
+ const aiConfigOptions = useAIConfigOptions();
+
+ return (
+
+
+ {aiConfigOptions.error && (
+
{aiConfigOptions.error.message}
+ )}
+
+ AI Provider Configuration
+
+
+ To continue, connect an OpenAI, Anthropic, or other LLM account
+
+
+ Changing settings during an active session will end the session
+
+ {aiConfigOptions.isLoading && (
+
+
+
+ )}
+ {aiConfigOptions.data && (
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/routes/_oh/route.tsx b/frontend/src/routes/_oh/route.tsx
new file mode 100644
index 00000000000..328c271add9
--- /dev/null
+++ b/frontend/src/routes/_oh/route.tsx
@@ -0,0 +1,100 @@
+import React from "react";
+import { useRouteError, isRouteErrorResponse, Outlet } from "@remix-run/react";
+import i18n from "#/i18n";
+import { WaitlistModal } from "#/components/waitlist-modal";
+import { AnalyticsConsentFormModal } from "#/components/analytics-consent-form-modal";
+import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
+import { useIsAuthed } from "#/hooks/query/use-is-authed";
+import { useAuth } from "#/context/auth-context";
+import { useUserPrefs } from "#/context/user-prefs-context";
+import { Sidebar } from "./sidebar";
+import { useConfig } from "#/hooks/query/use-config";
+
+export function ErrorBoundary() {
+ const error = useRouteError();
+
+ if (isRouteErrorResponse(error)) {
+ return (
+
+
{error.status}
+
{error.statusText}
+
+ {error.data instanceof Object
+ ? JSON.stringify(error.data)
+ : error.data}
+
+
+ );
+ }
+ if (error instanceof Error) {
+ return (
+
+
Uh oh, an error occurred!
+
{error.message}
+
+ );
+ }
+
+ return (
+
+
Uh oh, an unknown error occurred!
+
+ );
+}
+
+export default function MainApp() {
+ const { gitHubToken, clearToken } = useAuth();
+ const { settings } = useUserPrefs();
+
+ const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(
+ !localStorage.getItem("analytics-consent"),
+ );
+
+ const config = useConfig();
+ const {
+ data: isAuthed,
+ isFetched,
+ isFetching: isFetchingAuth,
+ } = useIsAuthed();
+
+ const gitHubAuthUrl = useGitHubAuthUrl({
+ gitHubToken,
+ appMode: config.data?.APP_MODE || null,
+ gitHubClientId: config.data?.GITHUB_CLIENT_ID || null,
+ });
+
+ React.useEffect(() => {
+ if (isFetched && !isAuthed) clearToken();
+ }, [isFetched, isAuthed]);
+
+ React.useEffect(() => {
+ if (settings.LANGUAGE) {
+ i18n.changeLanguage(settings.LANGUAGE);
+ }
+ }, [settings.LANGUAGE]);
+
+ const isInWaitlist =
+ !isFetchingAuth && !isAuthed && config.data?.APP_MODE === "saas";
+
+ return (
+
+
+
+
+
+
+
+ {isInWaitlist && (
+
+ )}
+ {consentFormIsOpen && (
+
setConsentFormIsOpen(false)}
+ />
+ )}
+
+ );
+}
diff --git a/frontend/src/routes/_oh/sidebar.tsx b/frontend/src/routes/_oh/sidebar.tsx
new file mode 100644
index 00000000000..b02974fa9e1
--- /dev/null
+++ b/frontend/src/routes/_oh/sidebar.tsx
@@ -0,0 +1,91 @@
+import React from "react";
+import { useLocation } from "react-router-dom";
+import { LoadingSpinner } from "#/components/modals/loading-project";
+import { UserActions } from "#/components/user-actions";
+import { useAuth } from "#/context/auth-context";
+import { useUserPrefs } from "#/context/user-prefs-context";
+import { useGitHubUser } from "#/hooks/query/use-github-user";
+import { useIsAuthed } from "#/hooks/query/use-is-authed";
+import { SettingsModal } from "./modals/settings-modal";
+import { ExitProjectConfirmationModal } from "./modals/exit-project-confirmation-modal";
+import { AllHandsLogoButton } from "./buttons/all-hands-logo-button";
+import { SettingsButton } from "./buttons/settings-button";
+import { DocsButton } from "./buttons/docs-button";
+import { ExitProjectButton } from "./buttons/exit-project-button";
+import { AccountSettingsModal } from "./modals/account-settings-modal";
+
+export function Sidebar() {
+ const location = useLocation();
+
+ const user = useGitHubUser();
+ const { data: isAuthed } = useIsAuthed();
+
+ const { token, logout } = useAuth();
+ const { settingsAreUpToDate } = useUserPrefs();
+
+ const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
+ React.useState(false);
+ const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
+ const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] =
+ React.useState(false);
+
+ React.useEffect(() => {
+ // If the github token is invalid, open the account settings modal again
+ if (user.isError) {
+ setAccountSettingsModalOpen(true);
+ }
+ }, [user.isError]);
+
+ const handleAccountSettingsModalClose = () => {
+ // If the user closes the modal without connecting to GitHub,
+ // we need to log them out to clear the invalid token from the
+ // local storage
+ if (user.isError) logout();
+ setAccountSettingsModalOpen(false);
+ };
+
+ const handleClickLogo = () => {
+ if (location.pathname.startsWith("/app"))
+ setStartNewProjectModalIsOpen(true);
+ };
+
+ const showSettingsModal =
+ isAuthed && (!settingsAreUpToDate || settingsModalIsOpen);
+
+ return (
+ <>
+
+ {accountSettingsModalOpen && (
+
+ )}
+ {showSettingsModal && (
+ setSettingsModalIsOpen(false)} />
+ )}
+ {startNewProjectModalIsOpen && (
+ setStartNewProjectModalIsOpen(false)}
+ />
+ )}
+ >
+ );
+}
diff --git a/frontend/src/sessions.ts b/frontend/src/sessions.ts
deleted file mode 100644
index 612be965157..00000000000
--- a/frontend/src/sessions.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { createCookieSessionStorage } from "@remix-run/node";
-import { Settings } from "./services/settings";
-
-type SessionData = {
- tosAccepted: boolean;
- ghToken: string;
- token: string; // Session token
-};
-
-export const { getSession, commitSession, destroySession } =
- createCookieSessionStorage({
- cookie: {
- name: "__session",
- secrets: ["some_secret"],
- },
- });
-
-type SettingsSessionData = { settings: Settings };
-
-export const {
- getSession: getSettingsSession,
- commitSession: commitSettingsSession,
- destroySession: destroySettingsSession,
-} = createCookieSessionStorage({
- cookie: {
- name: "__settings",
- secrets: ["some_other_secret"],
- },
-});