+
+
diff --git a/ui/src/components/Layout.test.tsx b/ui/src/components/Layout.test.tsx
new file mode 100644
index 00000000000..cdb91dd2c35
--- /dev/null
+++ b/ui/src/components/Layout.test.tsx
@@ -0,0 +1,248 @@
+// @vitest-environment jsdom
+
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { Layout } from "./Layout";
+
+const mockHealthApi = vi.hoisted(() => ({
+ get: vi.fn(),
+}));
+
+const mockInstanceSettingsApi = vi.hoisted(() => ({
+ getGeneral: vi.fn(),
+}));
+
+const mockNavigate = vi.hoisted(() => vi.fn());
+const mockSetSelectedCompanyId = vi.hoisted(() => vi.fn());
+const mockSetSidebarOpen = vi.hoisted(() => vi.fn());
+let currentPathname = "/PAP/dashboard";
+
+vi.mock("@/lib/router", () => ({
+ Outlet: () =>
Outlet content
,
+ useLocation: () => ({ pathname: currentPathname, search: "", hash: "" }),
+ useNavigate: () => mockNavigate,
+ useParams: () => ({ companyPrefix: "PAP" }),
+}));
+
+vi.mock("./CompanyRail", () => ({
+ CompanyRail: () =>
Company rail
,
+}));
+
+vi.mock("./Sidebar", () => ({
+ Sidebar: () =>
Main company nav
,
+}));
+
+vi.mock("./InstanceSidebar", () => ({
+ InstanceSidebar: () =>
Instance sidebar
,
+}));
+
+vi.mock("./CompanySettingsSidebar", () => ({
+ CompanySettingsSidebar: () =>
Company settings sidebar
,
+}));
+
+vi.mock("./BreadcrumbBar", () => ({
+ BreadcrumbBar: () =>
Breadcrumbs
,
+}));
+
+vi.mock("./PropertiesPanel", () => ({
+ PropertiesPanel: () => null,
+}));
+
+vi.mock("./CommandPalette", () => ({
+ CommandPalette: () => null,
+}));
+
+vi.mock("./NewIssueDialog", () => ({
+ NewIssueDialog: () => null,
+}));
+
+vi.mock("./NewProjectDialog", () => ({
+ NewProjectDialog: () => null,
+}));
+
+vi.mock("./NewGoalDialog", () => ({
+ NewGoalDialog: () => null,
+}));
+
+vi.mock("./NewAgentDialog", () => ({
+ NewAgentDialog: () => null,
+}));
+
+vi.mock("./KeyboardShortcutsCheatsheet", () => ({
+ KeyboardShortcutsCheatsheet: () => null,
+}));
+
+vi.mock("./ToastViewport", () => ({
+ ToastViewport: () => null,
+}));
+
+vi.mock("./MobileBottomNav", () => ({
+ MobileBottomNav: () => null,
+}));
+
+vi.mock("./WorktreeBanner", () => ({
+ WorktreeBanner: () => null,
+}));
+
+vi.mock("./DevRestartBanner", () => ({
+ DevRestartBanner: () => null,
+}));
+
+vi.mock("./SidebarAccountMenu", () => ({
+ SidebarAccountMenu: () =>
Account menu
,
+}));
+
+vi.mock("../context/DialogContext", () => ({
+ useDialog: () => ({
+ openNewIssue: vi.fn(),
+ openOnboarding: vi.fn(),
+ }),
+}));
+
+vi.mock("../context/PanelContext", () => ({
+ usePanel: () => ({
+ togglePanelVisible: vi.fn(),
+ }),
+}));
+
+vi.mock("../context/CompanyContext", () => ({
+ useCompany: () => ({
+ companies: [{ id: "company-1", issuePrefix: "PAP", name: "Paperclip" }],
+ loading: false,
+ selectedCompany: { id: "company-1", issuePrefix: "PAP", name: "Paperclip" },
+ selectedCompanyId: "company-1",
+ selectionSource: "manual",
+ setSelectedCompanyId: mockSetSelectedCompanyId,
+ }),
+}));
+
+vi.mock("../context/SidebarContext", () => ({
+ useSidebar: () => ({
+ sidebarOpen: true,
+ setSidebarOpen: mockSetSidebarOpen,
+ toggleSidebar: vi.fn(),
+ isMobile: false,
+ }),
+}));
+
+vi.mock("../hooks/useKeyboardShortcuts", () => ({
+ useKeyboardShortcuts: () => undefined,
+}));
+
+vi.mock("../hooks/useCompanyPageMemory", () => ({
+ useCompanyPageMemory: () => undefined,
+}));
+
+vi.mock("../api/health", () => ({
+ healthApi: mockHealthApi,
+}));
+
+vi.mock("../api/instanceSettings", () => ({
+ instanceSettingsApi: mockInstanceSettingsApi,
+}));
+
+vi.mock("../lib/company-selection", () => ({
+ shouldSyncCompanySelectionFromRoute: () => false,
+}));
+
+vi.mock("../lib/instance-settings", () => ({
+ DEFAULT_INSTANCE_SETTINGS_PATH: "/instance/settings/general",
+ normalizeRememberedInstanceSettingsPath: (value: string | null | undefined) =>
+ value ?? "/instance/settings/general",
+}));
+
+vi.mock("../lib/main-content-focus", () => ({
+ scheduleMainContentFocus: () => () => undefined,
+}));
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+
+async function flushReact() {
+ await act(async () => {
+ await Promise.resolve();
+ await new Promise((resolve) => window.setTimeout(resolve, 0));
+ });
+}
+
+describe("Layout", () => {
+ let container: HTMLDivElement;
+
+ beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ currentPathname = "/PAP/dashboard";
+ mockHealthApi.get.mockResolvedValue({
+ status: "ok",
+ deploymentMode: "authenticated",
+ deploymentExposure: "private",
+ version: "1.2.3",
+ });
+ mockInstanceSettingsApi.getGeneral.mockResolvedValue({
+ keyboardShortcuts: false,
+ });
+ });
+
+ afterEach(() => {
+ container.remove();
+ document.body.innerHTML = "";
+ vi.clearAllMocks();
+ });
+
+ it("does not render the deployment explainer in the shared layout", async () => {
+ const root = createRoot(container);
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ await flushReact();
+ await flushReact();
+
+ expect(mockHealthApi.get).toHaveBeenCalled();
+ expect(container.textContent).toContain("Breadcrumbs");
+ expect(container.textContent).toContain("Outlet content");
+ expect(container.textContent).not.toContain("Authenticated private");
+ expect(container.textContent).not.toContain(
+ "Sign-in is required and this instance is intended for private-network access.",
+ );
+
+ await act(async () => {
+ root.unmount();
+ });
+ });
+
+ it("renders the company settings sidebar on company settings routes", async () => {
+ currentPathname = "/PAP/company/settings/access";
+ const root = createRoot(container);
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ await flushReact();
+ await flushReact();
+
+ expect(container.textContent).toContain("Company settings sidebar");
+ expect(container.textContent).not.toContain("Instance sidebar");
+ expect(container.textContent).not.toContain("Main company nav");
+
+ await act(async () => {
+ root.unmount();
+ });
+ });
+});
diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx
index b5809f464bf..2d9740484aa 100644
--- a/ui/src/components/Layout.tsx
+++ b/ui/src/components/Layout.tsx
@@ -1,10 +1,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
-import { BookOpen, Moon, Settings, Sun } from "lucide-react";
-import { Link, Outlet, useLocation, useNavigate, useParams } from "@/lib/router";
+import { Outlet, useLocation, useNavigate, useParams } from "@/lib/router";
import { CompanyRail } from "./CompanyRail";
import { Sidebar } from "./Sidebar";
import { InstanceSidebar } from "./InstanceSidebar";
+import { CompanySettingsSidebar } from "./CompanySettingsSidebar";
import { BreadcrumbBar } from "./BreadcrumbBar";
import { PropertiesPanel } from "./PropertiesPanel";
import { CommandPalette } from "./CommandPalette";
@@ -17,12 +17,12 @@ import { ToastViewport } from "./ToastViewport";
import { MobileBottomNav } from "./MobileBottomNav";
import { WorktreeBanner } from "./WorktreeBanner";
import { DevRestartBanner } from "./DevRestartBanner";
+import { SidebarAccountMenu } from "./SidebarAccountMenu";
import { useDialog } from "../context/DialogContext";
import { GeneralSettingsProvider } from "../context/GeneralSettingsContext";
import { usePanel } from "../context/PanelContext";
import { useCompany } from "../context/CompanyContext";
import { useSidebar } from "../context/SidebarContext";
-import { useTheme } from "../context/ThemeContext";
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
import { healthApi } from "../api/health";
@@ -36,8 +36,6 @@ import { queryKeys } from "../lib/queryKeys";
import { scheduleMainContentFocus } from "../lib/main-content-focus";
import { cn } from "../lib/utils";
import { NotFoundPage } from "../pages/NotFound";
-import { Button } from "@/components/ui/button";
-import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
const INSTANCE_SETTINGS_MEMORY_KEY = "paperclip.lastInstanceSettingsPath";
@@ -62,17 +60,16 @@ export function Layout() {
selectionSource,
setSelectedCompanyId,
} = useCompany();
- const { theme, toggleTheme } = useTheme();
const { companyPrefix } = useParams<{ companyPrefix: string }>();
const navigate = useNavigate();
const location = useLocation();
const isInstanceSettingsRoute = location.pathname.startsWith("/instance/");
+ const isCompanySettingsRoute = location.pathname.includes("/company/settings");
const onboardingTriggered = useRef(false);
const lastMainScrollTop = useRef(0);
const [mobileNavVisible, setMobileNavVisible] = useState(true);
const [instanceSettingsTarget, setInstanceSettingsTarget] = useState
(() => readRememberedInstanceSettingsPath());
const [shortcutsOpen, setShortcutsOpen] = useState(false);
- const nextTheme = theme === "dark" ? "light" : "dark";
const matchedCompany = useMemo(() => {
if (!companyPrefix) return null;
const requestedPrefix = companyPrefix.toUpperCase();
@@ -310,52 +307,19 @@ export function Layout() {
>
- {isInstanceSettingsRoute ? : }
-
-
-
-
-
- Documentation
-
- {health?.version && (
-
-
- v
-
- v{health.version}
-
- )}
-
-
-
+ {isInstanceSettingsRoute ? (
+
+ ) : isCompanySettingsRoute ? (
+
+ ) : (
+
+ )}
+
) : (
@@ -367,53 +331,20 @@ export function Layout() {
sidebarOpen ? "w-60" : "w-0"
)}
>
- {isInstanceSettingsRoute ? : }
-
-
-