diff --git a/crates/web/src/assets/css/components.css b/crates/web/src/assets/css/components.css
index e3731813b..094a7f03b 100644
--- a/crates/web/src/assets/css/components.css
+++ b/crates/web/src/assets/css/components.css
@@ -1113,6 +1113,10 @@
-webkit-mask-image: url("../icons/masks/mask-2f9b873ab7f9e17f.svg");
mask-image: url("../icons/masks/mask-2f9b873ab7f9e17f.svg");
}
+.settings-nav-item[data-section="projects"]::before {
+ -webkit-mask-image: url("../icons/masks/mask-9efc91f08692af1a.svg");
+ mask-image: url("../icons/masks/mask-9efc91f08692af1a.svg");
+}
.settings-nav-item[data-section="environment"]::before,
.settings-nav-item[data-section="terminal"]::before {
-webkit-mask-image: url("../icons/masks/mask-2920334234a725b2.svg");
diff --git a/crates/web/src/assets/js/page-projects.js b/crates/web/src/assets/js/page-projects.js
index dde144a03..3f031b8d6 100644
--- a/crates/web/src/assets/js/page-projects.js
+++ b/crates/web/src/assets/js/page-projects.js
@@ -17,6 +17,7 @@ var completions = signal([]);
var editingProject = signal(null);
var detecting = signal(false);
var clearing = signal(false);
+var _projectsContainer = null;
function PathInput(props) {
var inputRef = useRef(null);
@@ -316,17 +317,19 @@ function ProjectsPage() {
`;
}
-registerPage(
- routes.projects,
- function initProjects(container) {
- container.style.cssText = "flex-direction:column;padding:0;overflow:hidden;";
- editingProject.value = null;
- completions.value = [];
- detecting.value = false;
- render(html`<${ProjectsPage} />`, container);
- },
- function teardownProjects() {
- var container = S.$("pageContent");
- if (container) render(null, container);
- },
-);
+export function initProjects(container) {
+ _projectsContainer = container;
+ container.style.cssText = "flex-direction:column;padding:0;overflow:hidden;";
+ editingProject.value = null;
+ completions.value = [];
+ detecting.value = false;
+ clearing.value = false;
+ render(html`<${ProjectsPage} />`, container);
+}
+
+export function teardownProjects() {
+ if (_projectsContainer) render(null, _projectsContainer);
+ _projectsContainer = null;
+}
+
+registerPage(routes.projects, initProjects, teardownProjects);
diff --git a/crates/web/src/assets/js/page-settings.js b/crates/web/src/assets/js/page-settings.js
index 7c12d64db..bbebf2448 100644
--- a/crates/web/src/assets/js/page-settings.js
+++ b/crates/web/src/assets/js/page-settings.js
@@ -21,6 +21,7 @@ import { initMcp, teardownMcp } from "./page-mcp.js";
import { initMonitoring, teardownMonitoring } from "./page-metrics.js";
import { initNetworkAudit, teardownNetworkAudit } from "./page-network-audit.js";
import { initNodes, teardownNodes } from "./page-nodes.js";
+import { initProjects, teardownProjects } from "./page-projects.js";
import { initProviders, teardownProviders } from "./page-providers.js";
import { initSkills, teardownSkills } from "./page-skills.js";
import { initTerminal, teardownTerminal } from "./page-terminal.js";
@@ -119,6 +120,12 @@ var sections = [
icon: html``,
page: true,
},
+ {
+ id: "projects",
+ label: "Projects",
+ icon: html``,
+ page: true,
+ },
{
id: "environment",
label: "Environment",
@@ -5333,6 +5340,7 @@ var pageSectionHandlers = {
channels: { init: initChannels, teardown: teardownChannels },
mcp: { init: initMcp, teardown: teardownMcp },
nodes: { init: initNodes, teardown: teardownNodes },
+ projects: { init: initProjects, teardown: teardownProjects },
hooks: { init: initHooks, teardown: teardownHooks },
skills: { init: initSkills, teardown: teardownSkills },
agents: { init: initAgents, teardown: teardownAgents },
diff --git a/crates/web/ui/e2e/specs/projects.spec.js b/crates/web/ui/e2e/specs/projects.spec.js
index 9206d1bf8..047ba9dd2 100644
--- a/crates/web/ui/e2e/specs/projects.spec.js
+++ b/crates/web/ui/e2e/specs/projects.spec.js
@@ -32,6 +32,14 @@ test.describe("Projects page", () => {
await expect(page.locator('a.nav-link[href="/projects"]')).toHaveCount(0);
});
+ test("projects accessible from settings sidebar", async ({ page }) => {
+ const pageErrors = watchPageErrors(page);
+ await navigateAndWait(page, "/settings/projects");
+
+ await expect(page.getByRole("heading", { name: "Repositories", exact: true })).toBeVisible();
+ expect(pageErrors).toEqual([]);
+ });
+
test("page has no JS errors", async ({ page }) => {
const pageErrors = watchPageErrors(page);
await navigateAndWait(page, "/projects");
diff --git a/crates/web/ui/e2e/specs/settings-nav.spec.js b/crates/web/ui/e2e/specs/settings-nav.spec.js
index 8e8274561..1c6fcabbd 100644
--- a/crates/web/ui/e2e/specs/settings-nav.spec.js
+++ b/crates/web/ui/e2e/specs/settings-nav.spec.js
@@ -184,6 +184,7 @@ test.describe("Settings navigation", () => {
{ id: "mcp", heading: "MCP" },
{ id: "hooks", heading: "Hooks" },
{ id: "skills", heading: "Skills" },
+ { id: "projects", heading: "Repositories" },
{ id: "sandboxes", heading: "Sandboxes" },
{ id: "monitoring", heading: "Monitoring" },
{ id: "logs", heading: "Logs" },