浏览代码

feat(frontend): Improve file based routing (#4317)

sp.wack 1 年之前
父节点
当前提交
2277897f86

+ 0 - 43
frontend/__tests__/routes/app._index.test.tsx

@@ -1,43 +0,0 @@
-import { createRemixStub } from "@remix-run/testing";
-import { describe, expect, it } from "vitest";
-import { screen, within } from "@testing-library/react";
-import { renderWithProviders } from "test-utils";
-import userEvent from "@testing-library/user-event";
-import CodeEditor from "#/routes/app._index/route";
-
-const RemixStub = createRemixStub([{ path: "/app", Component: CodeEditor }]);
-
-describe.skip("CodeEditor", () => {
-  it("should render", async () => {
-    renderWithProviders(<RemixStub initialEntries={["/app"]} />);
-    await screen.findByTestId("file-explorer");
-    expect(screen.getByTestId("code-editor-empty-message")).toBeInTheDocument();
-  });
-
-  it("should retrieve the files", async () => {
-    renderWithProviders(<RemixStub initialEntries={["/app"]} />);
-    const explorer = await screen.findByTestId("file-explorer");
-
-    const files = within(explorer).getAllByTestId("tree-node");
-    // request mocked with msw
-    expect(files).toHaveLength(3);
-  });
-
-  it("should open a file", async () => {
-    const user = userEvent.setup();
-    renderWithProviders(<RemixStub initialEntries={["/app"]} />);
-    const explorer = await screen.findByTestId("file-explorer");
-
-    const files = within(explorer).getAllByTestId("tree-node");
-    await user.click(files[0]);
-
-    // check if the file is opened
-    expect(
-      screen.queryByTestId("code-editor-empty-message"),
-    ).not.toBeInTheDocument();
-    const editor = await screen.findByTestId("code-editor");
-    expect(
-      within(editor).getByText(/content of file1.ts/i),
-    ).toBeInTheDocument();
-  });
-});

+ 0 - 56
frontend/__tests__/routes/app.test.tsx

@@ -1,56 +0,0 @@
-import { createRemixStub } from "@remix-run/testing";
-import { beforeAll, describe, expect, it, vi } from "vitest";
-import { render, screen, waitFor } from "@testing-library/react";
-import { ws } from "msw";
-import { setupServer } from "msw/node";
-import App from "#/routes/app";
-import AgentState from "#/types/AgentState";
-import { AgentStateChangeObservation } from "#/types/core/observations";
-
-const RemixStub = createRemixStub([{ path: "/app", Component: App }]);
-
-describe.skip("App", () => {
-  const agent = ws.link("ws://localhost:3001/ws");
-  const server = setupServer();
-
-  beforeAll(() => {
-    // mock `dom.scrollTo`
-    HTMLElement.prototype.scrollTo = vi.fn().mockImplementation(() => {});
-  });
-
-  it("should render", async () => {
-    render(<RemixStub initialEntries={["/app"]} />);
-
-    await waitFor(() => {
-      expect(screen.getByTestId("app")).toBeInTheDocument();
-      expect(
-        screen.getByText(/INITIALIZING_AGENT_LOADING_MESSAGE/i),
-      ).toBeInTheDocument();
-    });
-  });
-
-  it("should establish a ws connection and send the init message", async () => {
-    server.use(
-      agent.addEventListener("connection", ({ client }) => {
-        client.send(
-          JSON.stringify({
-            id: 1,
-            cause: 0,
-            message: "AGENT_INIT_MESSAGE",
-            source: "agent",
-            timestamp: new Date().toISOString(),
-            observation: "agent_state_changed",
-            content: "AGENT_INIT_MESSAGE",
-            extras: { agent_state: AgentState.INIT },
-          } satisfies AgentStateChangeObservation),
-        );
-      }),
-    );
-
-    render(<RemixStub initialEntries={["/app"]} />);
-
-    await waitFor(() => {
-      expect(screen.getByText(/AGENT_INIT_MESSAGE/i)).toBeInTheDocument();
-    });
-  });
-});

+ 0 - 50
frontend/__tests__/routes/home.test.tsx

@@ -1,50 +0,0 @@
-import { createRemixStub } from "@remix-run/testing";
-import { describe, expect, it } from "vitest";
-import { render, screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import Home from "#/routes/_index/route";
-
-const renderRemixStub = (config?: { authenticated: boolean }) =>
-  createRemixStub([
-    {
-      path: "/",
-      Component: Home,
-      loader: () => ({
-        ghToken: config?.authenticated ? "ghp_123456" : null,
-      }),
-    },
-  ]);
-
-describe.skip("Home (_index)", () => {
-  it("should render", async () => {
-    const RemixStub = renderRemixStub();
-    render(<RemixStub />);
-    await screen.findByText(/let's start building/i);
-  });
-
-  it("should load the gh repos if a token is present", async () => {
-    const user = userEvent.setup();
-    const RemixStub = renderRemixStub({ authenticated: true });
-    render(<RemixStub />);
-
-    const repos = await screen.findByPlaceholderText(
-      /select a github project/i,
-    );
-    await user.click(repos);
-    // mocked responses from msw
-    screen.getByText(/octocat\/hello-world/i);
-    screen.getByText(/octocat\/earth/i);
-  });
-
-  it("should not load the gh repos if a token is not present", async () => {
-    const RemixStub = renderRemixStub();
-    render(<RemixStub />);
-
-    const repos = await screen.findByPlaceholderText(
-      /select a github project/i,
-    );
-    await userEvent.click(repos);
-    expect(screen.queryByText(/octocat\/hello-world/i)).not.toBeInTheDocument();
-    expect(screen.queryByText(/octocat\/earth/i)).not.toBeInTheDocument();
-  });
-});

+ 0 - 40
frontend/__tests__/routes/root.test.tsx

@@ -1,40 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { createRemixStub } from "@remix-run/testing";
-import { render, screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import App, { clientLoader } from "#/root";
-
-const RemixStub = createRemixStub([
-  {
-    path: "/",
-    Component: App,
-    loader: clientLoader,
-  },
-]);
-
-describe.skip("Root", () => {
-  it("should render", async () => {
-    render(<RemixStub />);
-    await screen.findByTestId("link-to-main");
-  });
-
-  describe("Auth Modal", () => {
-    it("should display the auth modal on first time visit", async () => {
-      render(<RemixStub />);
-      await screen.findByTestId("auth-modal");
-    });
-
-    it("should close the auth modal on accepting the terms", async () => {
-      const user = userEvent.setup();
-      render(<RemixStub />);
-      await screen.findByTestId("auth-modal");
-      await user.click(screen.getByTestId("accept-terms"));
-      await user.click(screen.getByRole("button", { name: /continue/i }));
-
-      expect(screen.queryByTestId("auth-modal")).not.toBeInTheDocument();
-      expect(screen.getByTestId("link-to-main")).toBeInTheDocument();
-    });
-
-    it.todo("should not display the auth modal on subsequent visits");
-  });
-});

+ 63 - 59
frontend/package-lock.json

@@ -1601,9 +1601,9 @@
       }
     },
     "node_modules/@jspm/core": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.0.1.tgz",
-      "integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.1.0.tgz",
+      "integrity": "sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==",
       "dev": true
     },
     "node_modules/@mdx-js/mdx": {
@@ -3560,15 +3560,15 @@
       }
     },
     "node_modules/@react-aria/grid": {
-      "version": "3.10.4",
-      "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.10.4.tgz",
-      "integrity": "sha512-3AjJ0hwRhOCIHThIZrGWrjAuKDpaZuBkODW3dvgLqtsNm3tL46DI6U9O3vfp8lNbrWMsXJgjRXwvXvdv0/gwCA==",
+      "version": "3.10.5",
+      "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.10.5.tgz",
+      "integrity": "sha512-9sLa+rpLgRZk7VX+tvdSudn1tdVgolVzhDLGWd95yS4UtPVMihTMGBrRoByY57Wxvh1V+7Ptw8kc6tsRSotYKg==",
       "dependencies": {
-        "@react-aria/focus": "^3.18.3",
+        "@react-aria/focus": "^3.18.4",
         "@react-aria/i18n": "^3.12.3",
-        "@react-aria/interactions": "^3.22.3",
+        "@react-aria/interactions": "^3.22.4",
         "@react-aria/live-announcer": "^3.4.0",
-        "@react-aria/selection": "^3.20.0",
+        "@react-aria/selection": "^3.20.1",
         "@react-aria/utils": "^3.25.3",
         "@react-stately/collections": "^3.11.0",
         "@react-stately/grid": "^3.9.3",
@@ -3584,11 +3584,11 @@
       }
     },
     "node_modules/@react-aria/grid/node_modules/@react-aria/focus": {
-      "version": "3.18.3",
-      "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.3.tgz",
-      "integrity": "sha512-WKUElg+5zS0D3xlVn8MntNnkzJql2J6MuzAMP8Sv5WTgFDse/XGR842dsxPTIyKKdrWVCRegCuwa4m3n/GzgJw==",
+      "version": "3.18.4",
+      "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.4.tgz",
+      "integrity": "sha512-91J35077w9UNaMK1cpMUEFRkNNz0uZjnSwiyBCFuRdaVuivO53wNC9XtWSDNDdcO5cGy87vfJRVAiyoCn/mjqA==",
       "dependencies": {
-        "@react-aria/interactions": "^3.22.3",
+        "@react-aria/interactions": "^3.22.4",
         "@react-aria/utils": "^3.25.3",
         "@react-types/shared": "^3.25.0",
         "@swc/helpers": "^0.5.0",
@@ -3617,9 +3617,9 @@
       }
     },
     "node_modules/@react-aria/grid/node_modules/@react-aria/interactions": {
-      "version": "3.22.3",
-      "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.3.tgz",
-      "integrity": "sha512-RRUb/aG+P0IKTIWikY/SylB6bIbLZeztnZY2vbe7RAG5MgVaCgn5HQ45SI15GlTmhsFG8CnF6slJsUFJiNHpbQ==",
+      "version": "3.22.4",
+      "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.4.tgz",
+      "integrity": "sha512-E0vsgtpItmknq/MJELqYJwib+YN18Qag8nroqwjk1qOnBa9ROIkUhWJerLi1qs5diXq9LHKehZDXRlwPvdEFww==",
       "dependencies": {
         "@react-aria/ssr": "^3.9.6",
         "@react-aria/utils": "^3.25.3",
@@ -3631,13 +3631,13 @@
       }
     },
     "node_modules/@react-aria/grid/node_modules/@react-aria/selection": {
-      "version": "3.20.0",
-      "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.20.0.tgz",
-      "integrity": "sha512-h3giMcXo4SMZRL5HrqZvOLNTsdh5jCXwLUx0wpj/2EF0tcYQL6WDfn1iJ+rHARkUIs7X70fUV8iwlbUySZy1xg==",
+      "version": "3.20.1",
+      "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.20.1.tgz",
+      "integrity": "sha512-My0w8UC/7PAkz/1yZUjr2VRuzDZz1RrbgTqP36j5hsJx8RczDTjI4TmKtQNKG0ggaP4w83G2Og5JPTq3w3LMAw==",
       "dependencies": {
-        "@react-aria/focus": "^3.18.3",
+        "@react-aria/focus": "^3.18.4",
         "@react-aria/i18n": "^3.12.3",
-        "@react-aria/interactions": "^3.22.3",
+        "@react-aria/interactions": "^3.22.4",
         "@react-aria/utils": "^3.25.3",
         "@react-stately/selection": "^3.17.0",
         "@react-types/shared": "^3.25.0",
@@ -4110,12 +4110,12 @@
       }
     },
     "node_modules/@react-aria/toggle": {
-      "version": "3.10.8",
-      "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.8.tgz",
-      "integrity": "sha512-N6WTgE8ByMYY+ZygUUPGON2vW5NrxwU91H98+Nozl+Rq6ZYR2fD9i8oRtLtrYPxjU2HmaFwDyQdWvmMJZuDxig==",
+      "version": "3.10.9",
+      "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.9.tgz",
+      "integrity": "sha512-dtfnyIU2/kcH9rFAiB48diSmaXDv45K7UCuTkMQLjbQa3QHC1oYNbleVN/VdGyAMBsIWtfl8L4uuPrAQmDV/bg==",
       "dependencies": {
-        "@react-aria/focus": "^3.18.3",
-        "@react-aria/interactions": "^3.22.3",
+        "@react-aria/focus": "^3.18.4",
+        "@react-aria/interactions": "^3.22.4",
         "@react-aria/utils": "^3.25.3",
         "@react-stately/toggle": "^3.7.8",
         "@react-types/checkbox": "^3.8.4",
@@ -4127,11 +4127,11 @@
       }
     },
     "node_modules/@react-aria/toggle/node_modules/@react-aria/focus": {
-      "version": "3.18.3",
-      "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.3.tgz",
-      "integrity": "sha512-WKUElg+5zS0D3xlVn8MntNnkzJql2J6MuzAMP8Sv5WTgFDse/XGR842dsxPTIyKKdrWVCRegCuwa4m3n/GzgJw==",
+      "version": "3.18.4",
+      "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.4.tgz",
+      "integrity": "sha512-91J35077w9UNaMK1cpMUEFRkNNz0uZjnSwiyBCFuRdaVuivO53wNC9XtWSDNDdcO5cGy87vfJRVAiyoCn/mjqA==",
       "dependencies": {
-        "@react-aria/interactions": "^3.22.3",
+        "@react-aria/interactions": "^3.22.4",
         "@react-aria/utils": "^3.25.3",
         "@react-types/shared": "^3.25.0",
         "@swc/helpers": "^0.5.0",
@@ -4142,9 +4142,9 @@
       }
     },
     "node_modules/@react-aria/toggle/node_modules/@react-aria/interactions": {
-      "version": "3.22.3",
-      "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.3.tgz",
-      "integrity": "sha512-RRUb/aG+P0IKTIWikY/SylB6bIbLZeztnZY2vbe7RAG5MgVaCgn5HQ45SI15GlTmhsFG8CnF6slJsUFJiNHpbQ==",
+      "version": "3.22.4",
+      "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.4.tgz",
+      "integrity": "sha512-E0vsgtpItmknq/MJELqYJwib+YN18Qag8nroqwjk1qOnBa9ROIkUhWJerLi1qs5diXq9LHKehZDXRlwPvdEFww==",
       "dependencies": {
         "@react-aria/ssr": "^3.9.6",
         "@react-aria/utils": "^3.25.3",
@@ -6619,9 +6619,9 @@
       }
     },
     "node_modules/acorn": {
-      "version": "8.12.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
-      "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
+      "version": "8.13.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
+      "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==",
       "dev": true,
       "bin": {
         "acorn": "bin/acorn"
@@ -7325,9 +7325,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001668",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz",
-      "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==",
+      "version": "1.0.30001669",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz",
+      "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==",
       "funding": [
         {
           "type": "opencollective",
@@ -8396,9 +8396,9 @@
       "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.36",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz",
-      "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw=="
+      "version": "1.5.39",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.39.tgz",
+      "integrity": "sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m+6/Grg=="
     },
     "node_modules/emoji-regex": {
       "version": "9.2.2",
@@ -9840,9 +9840,9 @@
       }
     },
     "node_modules/framer-motion": {
-      "version": "11.11.8",
-      "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.8.tgz",
-      "integrity": "sha512-mnGQNEoz99GtFXBBPw+Ag5K4FcfP5XrXxrxHz+iE4Lmg7W3sf2gKmGuvfkZCW/yIfcdv5vJd6KiSPETH1Pw68Q==",
+      "version": "11.11.9",
+      "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.9.tgz",
+      "integrity": "sha512-XpdZseuCrZehdHGuW22zZt3SF5g6AHJHJi7JwQIigOznW4Jg1n0oGPMJQheMaKLC+0rp5gxUKMRYI6ytd3q4RQ==",
       "peer": true,
       "dependencies": {
         "tslib": "^2.4.0"
@@ -10321,9 +10321,9 @@
       }
     },
     "node_modules/hast-util-to-jsx-runtime": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.1.tgz",
-      "integrity": "sha512-Rbemi1rzrkysSin0FDHZfsxYPoqLGHFfxFm28aOBHPibT7aqjy7kUgY636se9xbuCWUsFpWAYlmtGHQakiqtEA==",
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz",
+      "integrity": "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==",
       "dependencies": {
         "@types/estree": "^1.0.0",
         "@types/hast": "^3.0.0",
@@ -22713,13 +22713,17 @@
       }
     },
     "node_modules/string.prototype.includes": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz",
-      "integrity": "sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
+      "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
       "dev": true,
       "dependencies": {
-        "define-properties": "^1.1.3",
-        "es-abstract": "^1.17.5"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
       }
     },
     "node_modules/string.prototype.matchall": {
@@ -23499,9 +23503,9 @@
       }
     },
     "node_modules/tslib": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
-      "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
+      "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
     },
     "node_modules/turbo-stream": {
       "version": "2.4.0",
@@ -23661,9 +23665,9 @@
       }
     },
     "node_modules/undici": {
-      "version": "6.20.0",
-      "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.0.tgz",
-      "integrity": "sha512-AITZfPuxubm31Sx0vr8bteSalEbs9wQb/BOBi9FPlD9Qpd6HxZ4Q0+hI742jBhkPb4RT2v5MQzaW5VhRVyj+9A==",
+      "version": "6.20.1",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz",
+      "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==",
       "engines": {
         "node": ">=18.17"
       }

+ 4 - 4
frontend/src/components/controls.tsx

@@ -4,8 +4,8 @@ import React from "react";
 import AgentControlBar from "./AgentControlBar";
 import AgentStatusBar from "./AgentStatusBar";
 import { ProjectMenuCard } from "./project-menu/ProjectMenuCard";
-import { clientLoader as rootClientLoader } from "#/root";
-import { clientLoader as appClientLoader } from "#/routes/app";
+import { clientLoader as rootClientLoader } from "#/routes/_oh";
+import { clientLoader as appClientLoader } from "#/routes/_oh.app";
 import { isGitHubErrorReponse } from "#/api/github";
 
 interface ControlsProps {
@@ -19,8 +19,8 @@ export function Controls({
   showSecurityLock,
   lastCommitData,
 }: ControlsProps) {
-  const rootData = useRouteLoaderData<typeof rootClientLoader>("root");
-  const appData = useRouteLoaderData<typeof appClientLoader>("routes/app");
+  const rootData = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
+  const appData = useRouteLoaderData<typeof appClientLoader>("routes/_oh.app");
 
   const projectMenuCardData = React.useMemo(
     () =>

+ 2 - 2
frontend/src/components/modals/AccountSettingsModal.tsx

@@ -5,7 +5,7 @@ import ModalBody from "./ModalBody";
 import ModalButton from "../buttons/ModalButton";
 import FormFieldset from "../form/FormFieldset";
 import { CustomInput } from "../form/custom-input";
-import { clientLoader } from "#/root";
+import { clientLoader } from "#/routes/_oh";
 import { clientAction as settingsClientAction } from "#/routes/settings";
 import { clientAction as loginClientAction } from "#/routes/login";
 import { AvailableLanguages } from "#/i18n";
@@ -21,7 +21,7 @@ function AccountSettingsModal({
   selectedLanguage,
   gitHubError,
 }: AccountSettingsModalProps) {
-  const data = useRouteLoaderData<typeof clientLoader>("root");
+  const data = useRouteLoaderData<typeof clientLoader>("routes/_oh");
   const settingsFetcher = useFetcher<typeof settingsClientAction>({
     key: "settings",
   });

+ 2 - 3
frontend/src/components/modals/connect-to-github-modal.tsx

@@ -1,5 +1,4 @@
 import { useFetcher, useRouteLoaderData } from "@remix-run/react";
-import React from "react";
 import ModalBody from "./ModalBody";
 import { CustomInput } from "../form/custom-input";
 import ModalButton from "../buttons/ModalButton";
@@ -7,7 +6,7 @@ import {
   BaseModalDescription,
   BaseModalTitle,
 } from "./confirmation-modals/BaseModal";
-import { clientLoader } from "#/root";
+import { clientLoader } from "#/routes/_oh";
 import { clientAction } from "#/routes/login";
 
 interface ConnectToGitHubModalProps {
@@ -15,7 +14,7 @@ interface ConnectToGitHubModalProps {
 }
 
 export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) {
-  const data = useRouteLoaderData<typeof clientLoader>("root");
+  const data = useRouteLoaderData<typeof clientLoader>("routes/_oh");
   const fetcher = useFetcher<typeof clientAction>({ key: "login" });
 
   return (

+ 0 - 51
frontend/src/components/modals/push-to-github-modal.tsx

@@ -1,51 +0,0 @@
-import { useFetcher } from "@remix-run/react";
-import ModalButton from "../buttons/ModalButton";
-import { BaseModalTitle } from "./confirmation-modals/BaseModal";
-import ModalBody from "./ModalBody";
-import { CustomInput } from "../form/custom-input";
-import { clientAction } from "#/routes/create-repository";
-import { isGitHubErrorReponse } from "#/api/github";
-
-interface PushToGitHubModalProps {
-  token: string;
-  onClose: () => void;
-}
-
-export function PushToGitHubModal({ token, onClose }: PushToGitHubModalProps) {
-  const fetcher = useFetcher<typeof clientAction>();
-  const actionData = fetcher.data;
-
-  return (
-    <ModalBody>
-      <BaseModalTitle title="Push to GitHub" />
-      <fetcher.Form
-        method="POST"
-        action="/create-repository"
-        className="w-full flex flex-col gap-6"
-      >
-        {actionData && isGitHubErrorReponse(actionData) && (
-          <div className="text-red-500 text-sm">{actionData.message}</div>
-        )}
-        <input type="text" hidden name="ghToken" defaultValue={token} />
-        <CustomInput name="repositoryName" label="Repository Name" required />
-        <CustomInput
-          name="repositoryDescription"
-          label="Repository Description"
-        />
-        <div className="w-full flex flex-col gap-2">
-          <ModalButton
-            type="submit"
-            text="Create"
-            disabled={fetcher.state === "submitting"}
-            className="bg-[#4465DB] w-full"
-          />
-          <ModalButton
-            text="Close"
-            className="bg-[#737373] w-full"
-            onClick={onClose}
-          />
-        </div>
-      </fetcher.Form>
-    </ModalBody>
-  );
-}

+ 1 - 243
frontend/src/root.tsx

@@ -5,31 +5,11 @@ import {
   Outlet,
   Scripts,
   ScrollRestoration,
-  defer,
-  useFetcher,
-  useLoaderData,
-  useLocation,
-  useNavigation,
 } from "@remix-run/react";
 import "./tailwind.css";
 import "./index.css";
 import React from "react";
 import { Toaster } from "react-hot-toast";
-import CogTooth from "./assets/cog-tooth";
-import { SettingsForm } from "./components/form/settings-form";
-import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
-import { ModalBackdrop } from "#/components/modals/modal-backdrop";
-import { isGitHubErrorReponse, retrieveGitHubUser } from "./api/github";
-import OpenHands from "./api/open-hands";
-import LoadingProjectModal from "./components/modals/LoadingProject";
-import { getSettings, settingsAreUpToDate } from "./services/settings";
-import AccountSettingsModal from "./components/modals/AccountSettingsModal";
-import NewProjectIcon from "./assets/new-project.svg?react";
-import DocsIcon from "./assets/docs.svg?react";
-import i18n from "./i18n";
-import { useSocket } from "./context/socket";
-import { UserAvatar } from "./components/user-avatar";
-import { DangerModal } from "./components/modals/confirmation-modals/danger-modal";
 
 export function Layout({ children }: { children: React.ReactNode }) {
   return (
@@ -55,228 +35,6 @@ export const meta: MetaFunction = () => [
   { name: "description", content: "Let's Start Building!" },
 ];
 
-export const clientLoader = async () => {
-  let token = localStorage.getItem("token");
-  const ghToken = localStorage.getItem("ghToken");
-
-  let user: GitHubUser | GitHubErrorReponse | null = null;
-  if (ghToken) user = await retrieveGitHubUser(ghToken);
-
-  const settings = getSettings();
-  await i18n.changeLanguage(settings.LANGUAGE);
-
-  const settingsIsUpdated = settingsAreUpToDate();
-  if (!settingsIsUpdated) {
-    localStorage.removeItem("token");
-    token = null;
-  }
-
-  return defer({
-    token,
-    ghToken,
-    user,
-    settingsIsUpdated,
-    settings,
-  });
-};
-
 export default function App() {
-  const { stop, isConnected } = useSocket();
-  const navigation = useNavigation();
-  const location = useLocation();
-  const { token, user, settingsIsUpdated, settings } =
-    useLoaderData<typeof clientLoader>();
-  const loginFetcher = useFetcher({ key: "login" });
-  const logoutFetcher = useFetcher({ key: "logout" });
-  const endSessionFetcher = useFetcher({ key: "end-session" });
-
-  const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
-    React.useState(false);
-  const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
-  const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] =
-    React.useState(false);
-  const [data, setData] = React.useState<{
-    models: string[];
-    agents: string[];
-    securityAnalyzers: string[];
-  }>({
-    models: [],
-    agents: [],
-    securityAnalyzers: [],
-  });
-
-  React.useEffect(() => {
-    // We fetch this here instead of the data loader because the server seems to block
-    // the retrieval when the session is closing -- preventing the screen from rendering until
-    // the fetch is complete
-    (async () => {
-      const [models, agents, securityAnalyzers] = await Promise.all([
-        OpenHands.getModels(),
-        OpenHands.getAgents(),
-        OpenHands.getSecurityAnalyzers(),
-      ]);
-
-      setData({ models, agents, securityAnalyzers });
-    })();
-  }, []);
-
-  React.useEffect(() => {
-    // If the github token is invalid, open the account settings modal again
-    if (isGitHubErrorReponse(user)) {
-      setAccountSettingsModalOpen(true);
-    }
-  }, [user]);
-
-  React.useEffect(() => {
-    if (location.pathname === "/") {
-      // If the user is on the home page, we should stop the socket connection.
-      // This is relevant when the user redirects here for whatever reason.
-      if (isConnected) stop();
-    }
-  }, [location.pathname]);
-
-  const handleUserLogout = () => {
-    logoutFetcher.submit(
-      {},
-      {
-        method: "POST",
-        action: "/logout",
-      },
-    );
-  };
-
-  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 (isGitHubErrorReponse(user)) handleUserLogout();
-    setAccountSettingsModalOpen(false);
-  };
-
-  const handleEndSession = () => {
-    setStartNewProjectModalIsOpen(false);
-    // call new session action and redirect to '/'
-    endSessionFetcher.submit(new FormData(), {
-      method: "POST",
-      action: "/end-session",
-    });
-  };
-
-  return (
-    <div className="bg-root-primary p-3 h-screen min-w-[1024px] overflow-x-hidden flex gap-3">
-      <aside className="px-1 flex flex-col gap-[15px]">
-        <button
-          type="button"
-          aria-label="All Hands Logo"
-          onClick={() => {
-            if (location.pathname !== "/") setStartNewProjectModalIsOpen(true);
-          }}
-        >
-          <AllHandsLogo width={34} height={23} />
-        </button>
-        <nav className="py-[18px] flex flex-col items-center gap-[18px]">
-          <UserAvatar
-            user={user}
-            isLoading={loginFetcher.state !== "idle"}
-            onLogout={handleUserLogout}
-            handleOpenAccountSettingsModal={() =>
-              setAccountSettingsModalOpen(true)
-            }
-          />
-          <button
-            type="button"
-            className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
-            onClick={() => setSettingsModalIsOpen(true)}
-            aria-label="Settings"
-          >
-            <CogTooth />
-          </button>
-          <a
-            href="https://docs.all-hands.dev"
-            target="_blank"
-            rel="noreferrer noopener"
-            className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
-            aria-label="Documentation"
-          >
-            <DocsIcon width={28} height={28} />
-          </a>
-          {!!token && (
-            <button
-              type="button"
-              aria-label="Start new project"
-              onClick={() => setStartNewProjectModalIsOpen(true)}
-            >
-              <NewProjectIcon width={28} height={28} />
-            </button>
-          )}
-        </nav>
-      </aside>
-      <div className="h-full w-full relative">
-        <Outlet />
-        {navigation.state === "loading" && location.pathname !== "/" && (
-          <ModalBackdrop>
-            <LoadingProjectModal
-              message={
-                endSessionFetcher.state === "loading"
-                  ? "Ending session, please wait..."
-                  : undefined
-              }
-            />
-          </ModalBackdrop>
-        )}
-        {(!settingsIsUpdated || settingsModalIsOpen) && (
-          <ModalBackdrop onClose={() => setSettingsModalIsOpen(false)}>
-            <div className="bg-root-primary w-[384px] p-6 rounded-xl flex flex-col gap-2">
-              <span className="text-xl leading-6 font-semibold -tracking-[0.01em">
-                AI Provider Configuration
-              </span>
-              <p className="text-xs text-[#A3A3A3]">
-                To continue, connect an OpenAI, Anthropic, or other LLM account
-              </p>
-              {isConnected && (
-                <p className="text-xs text-danger">
-                  Changing settings during an active session will end the
-                  session
-                </p>
-              )}
-              <SettingsForm
-                settings={settings}
-                models={data.models}
-                agents={data.agents}
-                securityAnalyzers={data.securityAnalyzers}
-                onClose={() => setSettingsModalIsOpen(false)}
-              />
-            </div>
-          </ModalBackdrop>
-        )}
-        {accountSettingsModalOpen && (
-          <ModalBackdrop onClose={handleAccountSettingsModalClose}>
-            <AccountSettingsModal
-              onClose={handleAccountSettingsModalClose}
-              selectedLanguage={settings.LANGUAGE}
-              gitHubError={isGitHubErrorReponse(user)}
-            />
-          </ModalBackdrop>
-        )}
-        {startNewProjectModalIsOpen && (
-          <ModalBackdrop onClose={() => setStartNewProjectModalIsOpen(false)}>
-            <DangerModal
-              title="Are you sure you want to exit?"
-              description="You will lose any unsaved information."
-              buttons={{
-                danger: {
-                  text: "Exit Project",
-                  onClick: handleEndSession,
-                },
-                cancel: {
-                  text: "Cancel",
-                  onClick: () => setStartNewProjectModalIsOpen(false),
-                },
-              }}
-            />
-          </ModalBackdrop>
-        )}
-      </div>
-    </div>
-  );
+  return <Outlet />;
 }

+ 0 - 0
frontend/src/routes/_index/github-repo-selector.tsx → frontend/src/routes/_oh._index/github-repo-selector.tsx


+ 0 - 0
frontend/src/routes/_index/hero-heading.tsx → frontend/src/routes/_oh._index/hero-heading.tsx


+ 2 - 2
frontend/src/routes/_index/route.tsx → frontend/src/routes/_oh._index/route.tsx

@@ -24,7 +24,7 @@ import { ModalBackdrop } from "#/components/modals/modal-backdrop";
 import { LoadingSpinner } from "#/components/modals/LoadingProject";
 import store, { RootState } from "#/store";
 import { removeFile, setInitialQuery } from "#/state/initial-query-slice";
-import { clientLoader as rootClientLoader } from "#/root";
+import { clientLoader as rootClientLoader } from "#/routes/_oh";
 import { UploadedFilePreview } from "./uploaded-file-preview";
 
 interface AttachedFilesSliderProps {
@@ -101,7 +101,7 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
 };
 
 function Home() {
-  const rootData = useRouteLoaderData<typeof rootClientLoader>("root");
+  const rootData = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
   const navigation = useNavigation();
   const { repositories, githubAuthUrl } = useLoaderData<typeof clientLoader>();
   const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =

+ 0 - 0
frontend/src/routes/_index/suggestion-box.tsx → frontend/src/routes/_oh._index/suggestion-box.tsx


+ 0 - 0
frontend/src/routes/_index/task-form.tsx → frontend/src/routes/_oh._index/task-form.tsx


+ 0 - 0
frontend/src/routes/_index/uploaded-file-preview.tsx → frontend/src/routes/_oh._index/uploaded-file-preview.tsx


+ 0 - 0
frontend/src/routes/app._index/code-editor-component.tsx → frontend/src/routes/_oh.app._index/code-editor-component.tsx


+ 0 - 0
frontend/src/routes/app._index/route.tsx → frontend/src/routes/_oh.app._index/route.tsx


+ 0 - 0
frontend/src/routes/app.browser.tsx → frontend/src/routes/_oh.app.browser.tsx


+ 0 - 0
frontend/src/routes/app.jupyter.tsx → frontend/src/routes/_oh.app.jupyter.tsx


+ 2 - 2
frontend/src/routes/app.tsx → frontend/src/routes/_oh.app.tsx

@@ -39,7 +39,7 @@ import { isGitHubErrorReponse, retrieveLatestGitHubCommit } from "#/api/github";
 import OpenHands from "#/api/open-hands";
 import AgentState from "#/types/AgentState";
 import { base64ToBlob } from "#/utils/base64-to-blob";
-import { clientLoader as rootClientLoader } from "#/root";
+import { clientLoader as rootClientLoader } from "#/routes/_oh";
 import { clearJupyter } from "#/state/jupyterSlice";
 import { FilesProvider } from "#/context/files";
 
@@ -111,7 +111,7 @@ function App() {
   const { settings, token, ghToken, repo, q, lastCommit } =
     useLoaderData<typeof clientLoader>();
   const fetcher = useFetcher();
-  const data = useRouteLoaderData<typeof rootClientLoader>("root");
+  const data = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
 
   // To avoid re-rendering the component when the user object changes, we memoize the user ID.
   // We use this to ensure the github token is valid before exporting it to the terminal.

+ 284 - 0
frontend/src/routes/_oh.tsx

@@ -0,0 +1,284 @@
+import React from "react";
+import {
+  defer,
+  useRouteError,
+  isRouteErrorResponse,
+  useNavigation,
+  useLocation,
+  useLoaderData,
+  useFetcher,
+  Outlet,
+} from "@remix-run/react";
+import { retrieveGitHubUser, isGitHubErrorReponse } from "#/api/github";
+import OpenHands from "#/api/open-hands";
+import CogTooth from "#/assets/cog-tooth";
+import { SettingsForm } from "#/components/form/settings-form";
+import AccountSettingsModal from "#/components/modals/AccountSettingsModal";
+import { DangerModal } from "#/components/modals/confirmation-modals/danger-modal";
+import LoadingProjectModal from "#/components/modals/LoadingProject";
+import { ModalBackdrop } from "#/components/modals/modal-backdrop";
+import { UserAvatar } from "#/components/user-avatar";
+import { useSocket } from "#/context/socket";
+import i18n from "#/i18n";
+import { getSettings, settingsAreUpToDate } from "#/services/settings";
+import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
+import NewProjectIcon from "#/assets/new-project.svg?react";
+import DocsIcon from "#/assets/docs.svg?react";
+
+export const clientLoader = async () => {
+  let token = localStorage.getItem("token");
+  const ghToken = localStorage.getItem("ghToken");
+
+  let user: GitHubUser | GitHubErrorReponse | null = null;
+  if (ghToken) user = await retrieveGitHubUser(ghToken);
+
+  const settings = getSettings();
+  await i18n.changeLanguage(settings.LANGUAGE);
+
+  const settingsIsUpdated = settingsAreUpToDate();
+  if (!settingsIsUpdated) {
+    localStorage.removeItem("token");
+    token = null;
+  }
+
+  return defer({
+    token,
+    ghToken,
+    user,
+    settingsIsUpdated,
+    settings,
+  });
+};
+
+export function ErrorBoundary() {
+  const error = useRouteError();
+
+  if (isRouteErrorResponse(error)) {
+    return (
+      <div>
+        <h1>{error.status}</h1>
+        <p>{error.statusText}</p>
+        <pre>
+          {error.data instanceof Object
+            ? JSON.stringify(error.data)
+            : error.data}
+        </pre>
+      </div>
+    );
+  }
+  if (error instanceof Error) {
+    return (
+      <div>
+        <h1>Uh oh, an error occurred!</h1>
+        <pre>{error.message}</pre>
+      </div>
+    );
+  }
+
+  return (
+    <div>
+      <h1>Uh oh, an unknown error occurred!</h1>
+    </div>
+  );
+}
+
+export default function MainApp() {
+  const { stop, isConnected } = useSocket();
+  const navigation = useNavigation();
+  const location = useLocation();
+  const { token, user, settingsIsUpdated, settings } =
+    useLoaderData<typeof clientLoader>();
+  const loginFetcher = useFetcher({ key: "login" });
+  const logoutFetcher = useFetcher({ key: "logout" });
+  const endSessionFetcher = useFetcher({ key: "end-session" });
+
+  const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
+    React.useState(false);
+  const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
+  const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] =
+    React.useState(false);
+  const [data, setData] = React.useState<{
+    models: string[];
+    agents: string[];
+    securityAnalyzers: string[];
+  }>({
+    models: [],
+    agents: [],
+    securityAnalyzers: [],
+  });
+
+  React.useEffect(() => {
+    // We fetch this here instead of the data loader because the server seems to block
+    // the retrieval when the session is closing -- preventing the screen from rendering until
+    // the fetch is complete
+    (async () => {
+      const [models, agents, securityAnalyzers] = await Promise.all([
+        OpenHands.getModels(),
+        OpenHands.getAgents(),
+        OpenHands.getSecurityAnalyzers(),
+      ]);
+
+      setData({ models, agents, securityAnalyzers });
+    })();
+  }, []);
+
+  React.useEffect(() => {
+    // If the github token is invalid, open the account settings modal again
+    if (isGitHubErrorReponse(user)) {
+      setAccountSettingsModalOpen(true);
+    }
+  }, [user]);
+
+  React.useEffect(() => {
+    if (location.pathname === "/") {
+      // If the user is on the home page, we should stop the socket connection.
+      // This is relevant when the user redirects here for whatever reason.
+      if (isConnected) stop();
+    }
+  }, [location.pathname]);
+
+  const handleUserLogout = () => {
+    logoutFetcher.submit(
+      {},
+      {
+        method: "POST",
+        action: "/logout",
+      },
+    );
+  };
+
+  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 (isGitHubErrorReponse(user)) handleUserLogout();
+    setAccountSettingsModalOpen(false);
+  };
+
+  const handleEndSession = () => {
+    setStartNewProjectModalIsOpen(false);
+    // call new session action and redirect to '/'
+    endSessionFetcher.submit(new FormData(), {
+      method: "POST",
+      action: "/end-session",
+    });
+  };
+
+  return (
+    <div className="bg-root-primary p-3 h-screen min-w-[1024px] overflow-x-hidden flex gap-3">
+      <aside className="px-1 flex flex-col gap-[15px]">
+        <button
+          type="button"
+          aria-label="All Hands Logo"
+          onClick={() => {
+            if (location.pathname !== "/") setStartNewProjectModalIsOpen(true);
+          }}
+        >
+          <AllHandsLogo width={34} height={23} />
+        </button>
+        <nav className="py-[18px] flex flex-col items-center gap-[18px]">
+          <UserAvatar
+            user={user}
+            isLoading={loginFetcher.state !== "idle"}
+            onLogout={handleUserLogout}
+            handleOpenAccountSettingsModal={() =>
+              setAccountSettingsModalOpen(true)
+            }
+          />
+          <button
+            type="button"
+            className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
+            onClick={() => setSettingsModalIsOpen(true)}
+            aria-label="Settings"
+          >
+            <CogTooth />
+          </button>
+          <a
+            href="https://docs.all-hands.dev"
+            target="_blank"
+            rel="noreferrer noopener"
+            className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
+            aria-label="Documentation"
+          >
+            <DocsIcon width={28} height={28} />
+          </a>
+          {!!token && (
+            <button
+              type="button"
+              aria-label="Start new project"
+              onClick={() => setStartNewProjectModalIsOpen(true)}
+            >
+              <NewProjectIcon width={28} height={28} />
+            </button>
+          )}
+        </nav>
+      </aside>
+      <div className="h-full w-full relative">
+        <Outlet />
+        {navigation.state === "loading" && location.pathname !== "/" && (
+          <ModalBackdrop>
+            <LoadingProjectModal
+              message={
+                endSessionFetcher.state === "loading"
+                  ? "Ending session, please wait..."
+                  : undefined
+              }
+            />
+          </ModalBackdrop>
+        )}
+        {(!settingsIsUpdated || settingsModalIsOpen) && (
+          <ModalBackdrop onClose={() => setSettingsModalIsOpen(false)}>
+            <div className="bg-root-primary w-[384px] p-6 rounded-xl flex flex-col gap-2">
+              <span className="text-xl leading-6 font-semibold -tracking-[0.01em">
+                AI Provider Configuration
+              </span>
+              <p className="text-xs text-[#A3A3A3]">
+                To continue, connect an OpenAI, Anthropic, or other LLM account
+              </p>
+              {isConnected && (
+                <p className="text-xs text-danger">
+                  Changing settings during an active session will end the
+                  session
+                </p>
+              )}
+              <SettingsForm
+                settings={settings}
+                models={data.models}
+                agents={data.agents}
+                securityAnalyzers={data.securityAnalyzers}
+                onClose={() => setSettingsModalIsOpen(false)}
+              />
+            </div>
+          </ModalBackdrop>
+        )}
+        {accountSettingsModalOpen && (
+          <ModalBackdrop onClose={handleAccountSettingsModalClose}>
+            <AccountSettingsModal
+              onClose={handleAccountSettingsModalClose}
+              selectedLanguage={settings.LANGUAGE}
+              gitHubError={isGitHubErrorReponse(user)}
+            />
+          </ModalBackdrop>
+        )}
+        {startNewProjectModalIsOpen && (
+          <ModalBackdrop onClose={() => setStartNewProjectModalIsOpen(false)}>
+            <DangerModal
+              title="Are you sure you want to exit?"
+              description="You will lose any unsaved information."
+              buttons={{
+                danger: {
+                  text: "Exit Project",
+                  onClick: handleEndSession,
+                },
+                cancel: {
+                  text: "Cancel",
+                  onClick: () => setStartNewProjectModalIsOpen(false),
+                },
+              }}
+            />
+          </ModalBackdrop>
+        )}
+      </div>
+    </div>
+  );
+}

+ 0 - 23
frontend/src/routes/create-repository.ts

@@ -1,23 +0,0 @@
-import { ClientActionFunctionArgs, json } from "@remix-run/react";
-import { createGitHubRepository } from "#/api/github";
-
-export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
-  const formData = await request.formData();
-  const token = formData.get("ghToken")?.toString();
-  const repositoryName = formData.get("repositoryName")?.toString();
-  const repositoryDescription = formData
-    .get("repositoryDescription")
-    ?.toString();
-
-  if (token && repositoryName) {
-    const response = await createGitHubRepository(
-      token,
-      repositoryName,
-      repositoryDescription,
-    );
-
-    return json(response);
-  }
-
-  return json(null);
-};