diff --git a/ui/src/components/SidebarCompanyMenu.production.tsx b/ui/src/components/SidebarCompanyMenu.production.tsx
index 8d2a50a2e3..476fcf6e87 100644
--- a/ui/src/components/SidebarCompanyMenu.production.tsx
+++ b/ui/src/components/SidebarCompanyMenu.production.tsx
@@ -20,7 +20,7 @@ import {
} from "@dnd-kit/core";
import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
-import type { Company } from "@paperclipai/shared";
+import { hidesCompanyPage, type Company } from "@paperclipai/shared";
import { Link, useLocation, useNavigate } from "@/lib/router";
import { authApi } from "@/api/auth";
import { cloudApi, type CloudStackSummary } from "@/api/cloud";
@@ -36,6 +36,7 @@ import {
import { useCompany } from "@/context/CompanyContext";
import { useDialogActions } from "@/context/DialogContext";
import { useCloudInstance } from "@/hooks/useCloudInstance";
+import { useHiddenSettings } from "@/hooks/useHiddenSettings";
import { useCompanyOrder } from "@/hooks/useCompanyOrder";
import { useSignOut } from "@/hooks/useSignOut";
import { navigateTopLevel } from "@/lib/browserNavigation";
@@ -234,6 +235,18 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
// exactly one company, and switching means leaving this tenant host entirely.
const cloud = useCloudInstance();
const isCloud = Boolean(cloud);
+ // The invite shortcut points at the company Invites surface, so an operator
+ // that hides that surface via PAPERCLIP_HIDDEN_SETTINGS (company.invites or
+ // company.members) hides this shortcut too. This is the per-deployment knob
+ // Paperclip Cloud uses to drop the shortcut on its managed stacks while
+ // other hosters keep it; the streamlined menu already honors it, this shell
+ // was the gap. Until the health response resolves the hidden set is unknown
+ // — keep the shortcut out rather than flash it.
+ const { hidden: hiddenSettings, loaded: hiddenSettingsLoaded } = useHiddenSettings();
+ const showInvitePeople =
+ hiddenSettingsLoaded &&
+ !hidesCompanyPage(hiddenSettings, "company.members") &&
+ !hidesCompanyPage(hiddenSettings, "company.invites");
const cloudBaseUrl = cloud?.cloudBaseUrl ?? null;
const stacksQuery = useQuery({
queryKey: queryKeys.cloud.stacks,
@@ -477,23 +490,25 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
>
)}
-
- {
- if (isEditingOrder) {
- event.preventDefault();
- return;
- }
- closeNavigationChrome();
- }}
- >
-
-
- {currentName ? `Invite people to ${currentName}` : "Invite people"}
-
-
-
+ {showInvitePeople ? (
+
+ {
+ if (isEditingOrder) {
+ event.preventDefault();
+ return;
+ }
+ closeNavigationChrome();
+ }}
+ >
+
+
+ {currentName ? `Invite people to ${currentName}` : "Invite people"}
+
+
+
+ ) : null}
{session?.session ? (
<>
diff --git a/ui/src/components/SidebarCompanyMenu.test.tsx b/ui/src/components/SidebarCompanyMenu.test.tsx
index 133cfea4ab..5c6cd697ef 100644
--- a/ui/src/components/SidebarCompanyMenu.test.tsx
+++ b/ui/src/components/SidebarCompanyMenu.test.tsx
@@ -7,6 +7,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { queryKeys } from "@/lib/queryKeys";
import { SidebarCompanyMenu } from "./SidebarCompanyMenu";
+import { SidebarCompanyMenu as SidebarCompanyMenuProduction } from "./SidebarCompanyMenu.production";
const mockAuthApi = vi.hoisted(() => ({
getSession: vi.fn(),
@@ -370,6 +371,56 @@ describe("SidebarCompanyMenu", () => {
});
});
+ it("shows the production-shell invite shortcut when no surface is hidden", async () => {
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ queryClient.setQueryData(queryKeys.health, { status: "ok" });
+ const root = createRoot(container);
+ act(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+ await flushReact();
+ await flushReact();
+
+ await openMenu("Open Acme Labs company switcher");
+
+ expect(document.body.textContent).toContain("Invite people to Acme Labs");
+
+ act(() => {
+ root.unmount();
+ });
+ });
+
+ it("hides the production-shell invite shortcut when the operator hides the invites surface", async () => {
+ // The production shell (streamlined UI disabled) must honor
+ // PAPERCLIP_HIDDEN_SETTINGS like the streamlined menu — this is the knob
+ // Paperclip Cloud uses to drop the shortcut on its managed stacks.
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ queryClient.setQueryData(queryKeys.health, { status: "ok", hiddenSettings: ["company.invites"] });
+ const root = createRoot(container);
+ act(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+ await flushReact();
+ await flushReact();
+
+ await openMenu("Open Acme Labs company switcher");
+
+ expect(document.body.textContent).toContain("Switch");
+ expect(document.body.textContent).not.toContain("Invite people");
+
+ act(() => {
+ root.unmount();
+ });
+ });
+
it("keeps the invite shortcut out of the menu until hidden settings resolve", async () => {
// No health data in the cache: the hidden-settings set is unknown, so the
// shortcut must not flash in and then disappear once the response lands.