diff --git a/packages/shared/src/types/instance.ts b/packages/shared/src/types/instance.ts
index 038fac8068..f03ac7df6f 100644
--- a/packages/shared/src/types/instance.ts
+++ b/packages/shared/src/types/instance.ts
@@ -55,6 +55,7 @@ export interface InstanceExperimentalSettings {
enableExperimentalFileViewer: boolean;
enableCloudSync: boolean;
enableExternalObjects: boolean;
+ enableGoalsSidebarLink: boolean;
enableServerInfoDebugView: boolean;
autoRestartDevServerWhenIdle: boolean;
enableIssueGraphLivenessAutoRecovery: boolean;
diff --git a/packages/shared/src/validators/instance.test.ts b/packages/shared/src/validators/instance.test.ts
index f754afea62..d77a00bcfb 100644
--- a/packages/shared/src/validators/instance.test.ts
+++ b/packages/shared/src/validators/instance.test.ts
@@ -17,6 +17,12 @@ describe("instance experimental settings validators", () => {
expect(settings.enableWorkspaceBranchReconcileForward).toBe(false);
});
+ it("defaults the goals sidebar link off", () => {
+ const settings = instanceExperimentalSettingsSchema.parse({});
+
+ expect(settings.enableGoalsSidebarLink).toBe(false);
+ });
+
it("accepts server info debug view patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
@@ -36,4 +42,14 @@ describe("instance experimental settings validators", () => {
enableWorkspaceBranchReconcileForward: true,
});
});
+
+ it("accepts goals sidebar link patches", () => {
+ expect(
+ patchInstanceExperimentalSettingsSchema.parse({
+ enableGoalsSidebarLink: true,
+ }),
+ ).toEqual({
+ enableGoalsSidebarLink: true,
+ });
+ });
});
diff --git a/packages/shared/src/validators/instance.ts b/packages/shared/src/validators/instance.ts
index c405f217a5..18ea416118 100644
--- a/packages/shared/src/validators/instance.ts
+++ b/packages/shared/src/validators/instance.ts
@@ -49,6 +49,7 @@ export const instanceExperimentalSettingsSchema = z.object({
enableExperimentalFileViewer: z.boolean().default(false),
enableCloudSync: z.boolean().default(false),
enableExternalObjects: z.boolean().default(false),
+ enableGoalsSidebarLink: z.boolean().default(false),
enableServerInfoDebugView: z.boolean().default(false),
autoRestartDevServerWhenIdle: z.boolean().default(false),
enableIssueGraphLivenessAutoRecovery: z.boolean().default(false),
diff --git a/server/src/__tests__/instance-settings-routes.test.ts b/server/src/__tests__/instance-settings-routes.test.ts
index b9fa242b16..2c717b557e 100644
--- a/server/src/__tests__/instance-settings-routes.test.ts
+++ b/server/src/__tests__/instance-settings-routes.test.ts
@@ -81,6 +81,8 @@ describe("instance settings routes", () => {
enableIssuePlanDecompositions: false,
enableExperimentalFileViewer: false,
enableCloudSync: false,
+ enableExternalObjects: false,
+ enableGoalsSidebarLink: false,
enableServerInfoDebugView: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: true,
@@ -103,6 +105,7 @@ describe("instance settings routes", () => {
enableTaskWatchdogs: false,
enableCloudSync: false,
enableExternalObjects: false,
+ enableGoalsSidebarLink: false,
enableServerInfoDebugView: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: true,
@@ -123,6 +126,8 @@ describe("instance settings routes", () => {
enableIssuePlanDecompositions: true,
enableExperimentalFileViewer: true,
enableCloudSync: true,
+ enableExternalObjects: false,
+ enableGoalsSidebarLink: false,
enableServerInfoDebugView: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: true,
@@ -150,6 +155,7 @@ describe("instance settings routes", () => {
enableTaskWatchdogs: true,
enableCloudSync: true,
enableExternalObjects: false,
+ enableGoalsSidebarLink: false,
enableServerInfoDebugView: true,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: true,
@@ -205,6 +211,7 @@ describe("instance settings routes", () => {
enableTaskWatchdogs: false,
enableCloudSync: false,
enableExternalObjects: false,
+ enableGoalsSidebarLink: false,
enableServerInfoDebugView: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: true,
@@ -302,6 +309,24 @@ describe("instance settings routes", () => {
});
});
+ it("allows local board users to update the goals sidebar link", async () => {
+ const app = await createApp({
+ type: "board",
+ userId: "local-board",
+ source: "local_implicit",
+ isInstanceAdmin: true,
+ });
+
+ await request(app)
+ .patch("/api/instance/settings/experimental")
+ .send({ enableGoalsSidebarLink: true })
+ .expect(200);
+
+ expect(mockInstanceSettingsService.updateExperimental).toHaveBeenCalledWith({
+ enableGoalsSidebarLink: true,
+ });
+ });
+
it("allows local board users to update the server info debug view", async () => {
const app = await createApp({
type: "board",
diff --git a/server/src/__tests__/instance-settings-service.test.ts b/server/src/__tests__/instance-settings-service.test.ts
index 1ac0eeb4fb..3fbed144ba 100644
--- a/server/src/__tests__/instance-settings-service.test.ts
+++ b/server/src/__tests__/instance-settings-service.test.ts
@@ -10,6 +10,7 @@ describe("instance settings service", () => {
enableExperimentalFileViewer: true,
enableTaskWatchdogs: true,
enableCloudSync: true,
+ enableGoalsSidebarLink: true,
enableServerInfoDebugView: true,
autoRestartDevServerWhenIdle: true,
enableIssueGraphLivenessAutoRecovery: true,
@@ -27,6 +28,7 @@ describe("instance settings service", () => {
enableExperimentalFileViewer: true,
enableTaskWatchdogs: true,
enableCloudSync: true,
+ enableGoalsSidebarLink: true,
enableServerInfoDebugView: true,
autoRestartDevServerWhenIdle: true,
enableIssueGraphLivenessAutoRecovery: true,
@@ -60,6 +62,14 @@ describe("instance settings service", () => {
).toBe(false);
});
+ it("defaults enableGoalsSidebarLink to false for empty and legacy stored settings", () => {
+ expect(normalizeExperimentalSettings(undefined).enableGoalsSidebarLink).toBe(false);
+ expect(normalizeExperimentalSettings({}).enableGoalsSidebarLink).toBe(false);
+ expect(
+ normalizeExperimentalSettings({ enableStreamlinedLeftNavigation: true }).enableGoalsSidebarLink,
+ ).toBe(false);
+ });
+
it("defaults enableWorkspaceBranchReconcileForward to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableWorkspaceBranchReconcileForward).toBe(false);
expect(normalizeExperimentalSettings({}).enableWorkspaceBranchReconcileForward).toBe(false);
diff --git a/server/src/services/instance-settings.ts b/server/src/services/instance-settings.ts
index f61c966cb3..cdb0ef3047 100644
--- a/server/src/services/instance-settings.ts
+++ b/server/src/services/instance-settings.ts
@@ -54,6 +54,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableTaskWatchdogs: parsed.data.enableTaskWatchdogs ?? false,
enableCloudSync: parsed.data.enableCloudSync ?? false,
enableExternalObjects: parsed.data.enableExternalObjects ?? false,
+ enableGoalsSidebarLink: parsed.data.enableGoalsSidebarLink ?? false,
enableServerInfoDebugView: parsed.data.enableServerInfoDebugView ?? false,
autoRestartDevServerWhenIdle: parsed.data.autoRestartDevServerWhenIdle ?? false,
enableIssueGraphLivenessAutoRecovery: parsed.data.enableIssueGraphLivenessAutoRecovery ?? false,
@@ -74,6 +75,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableExperimentalFileViewer: false,
enableCloudSync: false,
enableExternalObjects: false,
+ enableGoalsSidebarLink: false,
enableServerInfoDebugView: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: false,
diff --git a/ui/src/components/Sidebar.test.tsx b/ui/src/components/Sidebar.test.tsx
index 077141c53a..8007cb2848 100644
--- a/ui/src/components/Sidebar.test.tsx
+++ b/ui/src/components/Sidebar.test.tsx
@@ -177,7 +177,7 @@ describe("Sidebar", () => {
const workSectionContainer = workSection?.parentElement?.parentElement;
expect(workSectionContainer?.textContent).toContain("Work");
expect(workSectionContainer?.textContent).toContain("Tasks");
- expect(workSectionContainer?.textContent).toContain("Goals");
+ expect(workSectionContainer?.textContent).not.toContain("Goals");
flushSync(() => {
root.unmount();
@@ -294,10 +294,8 @@ describe("Sidebar", () => {
expect(artifactsLink?.getAttribute("href")).toBe("/artifacts");
const navText = container.querySelector("nav")?.textContent ?? "";
- expect(navText).toContain("Goals");
expect(navText).toContain("Artifacts");
expect(navText).toContain("Skills");
- expect(navText.indexOf("Goals")).toBeLessThan(navText.indexOf("Artifacts"));
expect(navText.indexOf("Artifacts")).toBeLessThan(navText.indexOf("Skills"));
const sections = [...container.querySelectorAll("nav > div")];
@@ -311,6 +309,50 @@ describe("Sidebar", () => {
});
});
+ it("hides the Goals nav item by default", async () => {
+ mockInstanceSettingsApi.getExperimental.mockResolvedValue({
+ enableIsolatedWorkspaces: false,
+ enableGoalsSidebarLink: false,
+ });
+ const root = await renderSidebar();
+
+ expect([...container.querySelectorAll("nav a")].map((a) => a.textContent?.trim())).not.toContain("Goals");
+
+ flushSync(() => {
+ root.unmount();
+ });
+ });
+
+ it("reserves the Goals nav slot while experimental settings are loading", async () => {
+ mockInstanceSettingsApi.getExperimental.mockImplementation(() => new Promise(() => {}));
+ const root = await renderSidebar();
+
+ expect([...container.querySelectorAll("nav a")].map((a) => a.textContent?.trim())).not.toContain("Goals");
+ expect(container.querySelector('[data-testid="sidebar-goals-placeholder"]')).not.toBeNull();
+
+ flushSync(() => {
+ root.unmount();
+ });
+ });
+
+ it("shows the Goals nav item when the experimental setting is enabled", async () => {
+ mockInstanceSettingsApi.getExperimental.mockResolvedValue({
+ enableIsolatedWorkspaces: false,
+ enableGoalsSidebarLink: true,
+ });
+ const root = await renderSidebar();
+
+ const link = [...container.querySelectorAll("a")].find((anchor) => anchor.textContent === "Goals");
+ expect(link?.getAttribute("href")).toBe("/goals");
+
+ const navText = container.querySelector("nav")?.textContent ?? "";
+ expect(navText.indexOf("Goals")).toBeLessThan(navText.indexOf("Artifacts"));
+
+ flushSync(() => {
+ root.unmount();
+ });
+ });
+
it("places Timeline in the Company section", async () => {
mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: false });
const root = await renderSidebar();
diff --git a/ui/src/components/Sidebar.tsx b/ui/src/components/Sidebar.tsx
index 46d86fad47..d8f630d102 100644
--- a/ui/src/components/Sidebar.tsx
+++ b/ui/src/components/Sidebar.tsx
@@ -60,6 +60,8 @@ export function Sidebar() {
const liveRunCount = liveRuns?.length ?? 0;
const showWorkspacesLink = experimentalSettings?.enableIsolatedWorkspaces === true;
const showPipelines = experimentalSettings?.enablePipelines === true;
+ const goalsLinkPending = experimentalSettings === undefined;
+ const showGoalsLink = experimentalSettings?.enableGoalsSidebarLink === true;
// Streamlined left navigation (top-level Projects link + starred children) is
// now the standard product sidebar (PAP-12472). The former experimental
// opt-out was retired; classic per-project collapsible mode is no longer
@@ -181,7 +183,15 @@ export function Sidebar() {
{showPipelines ? (
+ Restore the Goals item in the main sidebar while the goals surface is being evaluated. +
+