diff --git a/desktop/src/shared/features/manifest.ts b/desktop/src/shared/features/manifest.ts index 4335cf3a90..1e6f48ae01 100644 --- a/desktop/src/shared/features/manifest.ts +++ b/desktop/src/shared/features/manifest.ts @@ -13,6 +13,7 @@ const FeatureDefinitionSchema = z.object({ id: z.string().min(1), name: z.string().min(1), description: z.string(), + defaultEnabled: z.boolean().optional(), platforms: z.array(FeaturePlatformSchema).optional(), }); diff --git a/desktop/src/shared/features/resolveEnabled.test.mjs b/desktop/src/shared/features/resolveEnabled.test.mjs index 69414249dd..d725444870 100644 --- a/desktop/src/shared/features/resolveEnabled.test.mjs +++ b/desktop/src/shared/features/resolveEnabled.test.mjs @@ -12,6 +12,17 @@ describe("resolveEnabled (preview-only)", () => { assert.equal(resolveEnabled("workflows", { workflows: true }), true); }); + it("uses an enabled manifest default when no override exists", () => { + assert.equal(resolveEnabled("workspaceRail", {}, true), true); + }); + + it("lets an explicit opt-out override an enabled default", () => { + assert.equal( + resolveEnabled("workspaceRail", { workspaceRail: false }, true), + false, + ); + }); + it("returns false when user explicitly opts out", () => { assert.equal(resolveEnabled("workflows", { workflows: false }), false); }); diff --git a/desktop/src/shared/features/resolveEnabled.ts b/desktop/src/shared/features/resolveEnabled.ts index 1b0e4665bb..e3c544ec50 100644 --- a/desktop/src/shared/features/resolveEnabled.ts +++ b/desktop/src/shared/features/resolveEnabled.ts @@ -7,11 +7,13 @@ * (see `useFeatureEnabled`). Once you're inside `resolveEnabled`, the * feature IS in the manifest — preview by definition. * - * Returns true only if the user has explicitly opted in via overrides. + * An explicit user override wins; otherwise the feature's manifest default is + * used (false when omitted). */ export function resolveEnabled( featureId: string, overrides: Record, + defaultEnabled = false, ): boolean { - return overrides[featureId] === true; + return overrides[featureId] ?? defaultEnabled; } diff --git a/desktop/src/shared/features/types.ts b/desktop/src/shared/features/types.ts index 266bae6f46..c452cd54b4 100644 --- a/desktop/src/shared/features/types.ts +++ b/desktop/src/shared/features/types.ts @@ -12,6 +12,8 @@ export interface FeatureDefinition { id: string; name: string; description: string; + /** Whether the preview is enabled when the user has not chosen an override */ + defaultEnabled?: boolean; /** If omitted, feature is available on all platforms */ platforms?: FeaturePlatform[]; } diff --git a/desktop/src/shared/features/useFeatureEnabled.ts b/desktop/src/shared/features/useFeatureEnabled.ts index 6e928b3be3..b0c9878d0b 100644 --- a/desktop/src/shared/features/useFeatureEnabled.ts +++ b/desktop/src/shared/features/useFeatureEnabled.ts @@ -82,7 +82,7 @@ export function useFeatureSnapshot(): Record { * * The manifest (`preview-features.json`) lists ONLY preview features: * - * - in manifest (preview): true only if the user opted in via overrides + * - in manifest (preview): explicit user override, then manifest default (off if omitted) * - NOT in manifest (stable): always true (fail-open) * * Membership in the manifest signals "this needs gating"; absence means @@ -102,7 +102,7 @@ export function useFeatureEnabled(featureId: string): boolean { return true; } - return resolveEnabled(featureId, overrides); + return resolveEnabled(featureId, overrides, feature.defaultEnabled); } /** diff --git a/preview-features.json b/preview-features.json index d7362197e6..33cc7a252c 100644 --- a/preview-features.json +++ b/preview-features.json @@ -28,6 +28,7 @@ { "id": "workspaceRail", "name": "Community Rail", + "defaultEnabled": true, "description": "Far-left rail for switching communities with cross-community unread indicators", "platforms": [ "desktop"