diff --git a/packages/shared/src/workspace-commands.test.ts b/packages/shared/src/workspace-commands.test.ts
index 9a5c610b68..11eca81c40 100644
--- a/packages/shared/src/workspace-commands.test.ts
+++ b/packages/shared/src/workspace-commands.test.ts
@@ -75,4 +75,55 @@ describe("workspace command helpers", () => {
expect(match).toBeNull();
});
+
+ it("matches an exposed dev runtime whose bind command was hardened to loopback", () => {
+ const workspaceRuntime = {
+ commands: [
+ { id: "web", name: "paperclip-dev", kind: "service", command: "pnpm dev --bind lan" },
+ ],
+ };
+ const command = findWorkspaceCommandDefinition(workspaceRuntime, "web");
+ expect(command).not.toBeNull();
+
+ const match = matchWorkspaceRuntimeServiceToCommand(command!, [
+ {
+ id: "runtime-web",
+ serviceName: "paperclip-dev",
+ command: "pnpm dev --bind loopback",
+ cwd: "/repo",
+ configIndex: null,
+ exposure: {
+ provider: "tailscale_https",
+ state: "ready",
+ publicUrl: "https://paperclip-dev.example.ts.net:42012",
+ hostname: "paperclip-dev.example.ts.net",
+ listeners: [],
+ brokerRef: "broker-1",
+ lastError: null,
+ updatedAt: "2026-08-20T00:00:00.000Z",
+ },
+ },
+ ]);
+
+ expect(match).toEqual(expect.objectContaining({ id: "runtime-web" }));
+ });
+
+ it("does not equate a loopback command with a lan command without managed exposure", () => {
+ const command = findWorkspaceCommandDefinition({
+ services: [{ name: "paperclip-dev", command: "pnpm dev --bind lan" }],
+ }, "service:paperclip-dev");
+
+ const match = matchWorkspaceRuntimeServiceToCommand(command!, [
+ {
+ id: "runtime-web",
+ serviceName: "paperclip-dev",
+ command: "pnpm dev --bind loopback",
+ cwd: "/repo",
+ configIndex: null,
+ exposure: null,
+ },
+ ]);
+
+ expect(match).toBeNull();
+ });
});
diff --git a/packages/shared/src/workspace-commands.ts b/packages/shared/src/workspace-commands.ts
index dd98ef3dc0..e044e95ab2 100644
--- a/packages/shared/src/workspace-commands.ts
+++ b/packages/shared/src/workspace-commands.ts
@@ -1,4 +1,9 @@
import type { WorkspaceCommandDefinition, WorkspaceRuntimeService } from "./types/workspace-runtime.js";
+import { forceLoopbackBindInCommand } from "./runtime-exposure/loopback-bind.js";
+
+type WorkspaceRuntimeServiceMatchCandidate =
+ & Pick
You can change this later in Instance Settings > General.
+You can change this later in Settings > General.
{termsUrl ? ( Access.", + body: "Refresh to sign in, or ask the existing admin to invite you from Settings -> Access.", }; } if (error?.status === 401) { diff --git a/ui/src/components/CompanySettingsSidebar.test.tsx b/ui/src/components/CompanySettingsSidebar.test.tsx index 92073fce3e..282280ddb4 100644 --- a/ui/src/components/CompanySettingsSidebar.test.tsx +++ b/ui/src/components/CompanySettingsSidebar.test.tsx @@ -121,7 +121,7 @@ describe("CompanySettingsSidebar", () => { vi.clearAllMocks(); }); - it("renders the company back link and the settings sections in the sidebar", async () => { + it("renders one unified settings list without company or instance headers", async () => { const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, @@ -137,9 +137,8 @@ describe("CompanySettingsSidebar", () => { await flushReact(); expect(container.textContent).toContain("Paperclip"); - expect(container.textContent).toContain("Company Settings"); - expect(container.textContent).toContain("Company settings"); - expect(container.textContent).toContain("Instance settings"); + expect(container.textContent).not.toContain("Company Settings"); + expect(container.textContent).not.toContain("Instance Settings"); expect(container.textContent).toContain("General"); expect(container.textContent).toContain("Environments"); expect(container.textContent).toContain("Export"); @@ -147,6 +146,8 @@ describe("CompanySettingsSidebar", () => { expect(container.textContent).toContain("Members"); expect(container.textContent).toContain("Invites"); expect(container.textContent).toContain("Secrets"); + expect(container.textContent).toContain("Access"); + expect(container.textContent).toContain("Heartbeats"); expect(container.textContent).not.toContain("Tools & Access"); expect(sidebarNavItemMock).toHaveBeenCalledWith( expect.objectContaining({ @@ -175,6 +176,20 @@ describe("CompanySettingsSidebar", () => { end: true, }), ); + expect(sidebarNavItemMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: "/company/settings/instance/access", + label: "Access", + end: true, + }), + ); + expect(sidebarNavItemMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: "/company/settings/instance/heartbeats", + label: "Heartbeats", + end: true, + }), + ); expect(sidebarNavItemMock).toHaveBeenCalledWith( expect.objectContaining({ to: "/company/settings/members", @@ -204,13 +219,11 @@ describe("CompanySettingsSidebar", () => { end: true, }), ); - expect(sidebarNavItemMock).toHaveBeenCalledWith( - expect.objectContaining({ - to: "/company/settings/instance/general", - label: "General", - end: true, - }), - ); + expect(new Set( + sidebarNavItemMock.mock.calls + .filter(([props]) => props.label === "General") + .map(([props]) => props.to), + )).toEqual(new Set(["/company/settings"])); expect(sidebarNavItemMock).toHaveBeenCalledWith( expect.objectContaining({ to: "/company/settings/instance/plugins", diff --git a/ui/src/components/CompanySettingsSidebar.tsx b/ui/src/components/CompanySettingsSidebar.tsx index 2d1cec667c..3bf6883719 100644 --- a/ui/src/components/CompanySettingsSidebar.tsx +++ b/ui/src/components/CompanySettingsSidebar.tsx @@ -9,7 +9,6 @@ import { MailPlus, MonitorCog, Puzzle, - Settings, Shield, SlidersHorizontal, Upload, @@ -86,22 +85,17 @@ export function CompanySettingsSidebar() {