From 0804cdb5c63b902899e19298c9de8d80803ef859 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 1 Jun 2026 22:57:19 -0700 Subject: [PATCH] Gate Feed behind Beta Features (mirror Dock), default off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feed was made unconditionally available by #3854 (5829da2d9, "Enable Feed by default"), which deleted its beta gating. This reinstates the gating — mirroring Dock exactly — so Feed is hidden from the right-sidebar mode switcher until the user opts in under Settings → Beta Features. Default is off, same as Dock. Only the gating-removal hunks of #3854 are reinstated; that PR's Feed focus-handling bugfixes (and the "All Activity" rename) are preserved. The Settings layer was refactored into Swift packages since #3854, so Feed is mirrored in both parallel registrations Dock now lives in: - RightSidebarBetaFeatureSettings: add feedEnabledKey / defaultFeedEnabled / isFeedEnabled mirroring the Dock members (rightSidebar.beta.feed.enabled, default false). - RightSidebarMode+Availability: gate .feed on feedEnabled, threaded through availableModes / isAvailable alongside dockEnabled (reverts to the pre-#3854 shape). - RightSidebarPanelView: read feedEnabled via @AppStorage and feed it into availability + refresh-on-change. - App-target search index (SettingsNavigation, SettingsSearchAliases) and CmuxSettingsUI package index (CuratedSettingEntry+Default, SettingsSectionID): register the feed beta setting, anchor, and aliases mirroring dock. - CmuxSettings BetaFeaturesCatalogSection: add rightSidebarFeed catalog key (cmux.json-configurable, default false). - CmuxSettingsUI BetaFeaturesSection: re-add the Feed toggle row (SettingsBetaFeedToggle) with on/off subtitle. - CommandPaletteSettingsToggle: add the betaFeatures.feed toggle command. - Localizable.xcstrings: restore settings.betaFeatures.feed / .subtitleOn / .subtitleOff (en + ja). The plural beta warning already covers >1 feature (Dock + Extensions), so it is unchanged. Tests: - FileExplorerStateModePersistenceTests / RightSidebarCommandPaletteTests: restore the gated assertions (disabled Feed clamps to Files; default mode set excludes Feed). - RightSidebarChromeHeightUITests: re-add the feed-enable launch arg so the feed secondary bar is reachable. - SettingsSidebarBetaBehaviorUITests: add a Feed toggle derived-subtitle test mirroring the Dock one. - SettingsRowAnchorResolutionTests: register the feed search anchor. Feed hooks (per-tool-use feed.push installers) are intentionally NOT gated on this flag — see PR description. Co-Authored-By: Claude Opus 4.8 --- .../Keys/BetaFeaturesCatalogSection.swift | 10 ++++ .../CuratedSettingEntry+Default.swift | 1 + .../Navigation/SettingsSectionID.swift | 2 +- .../Sections/BetaFeaturesSection.swift | 25 ++++++++- .../SettingsRowAnchorResolutionTests.swift | 1 + Resources/Localizable.xcstrings | 51 +++++++++++++++++ Sources/App/WorkspaceRuntimeSettings.swift | 7 +++ .../CommandPaletteSettingsToggle.swift | 11 ++++ Sources/RightSidebarMode+Availability.swift | 20 +++++-- Sources/RightSidebarPanelView.swift | 5 +- Sources/SettingsNavigation.swift | 2 + Sources/SettingsSearchAliases.swift | 3 +- ...ileExplorerStateModePersistenceTests.swift | 24 +++++++- .../RightSidebarCommandPaletteTests.swift | 8 ++- .../RightSidebarChromeHeightUITests.swift | 1 + .../SettingsSidebarBetaBehaviorUITests.swift | 56 ++++++++++++++++++- 16 files changed, 208 insertions(+), 19 deletions(-) diff --git a/Packages/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift b/Packages/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift index 1b241027fa20..b507c7e6782e 100644 --- a/Packages/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift +++ b/Packages/CmuxSettings/Sources/CmuxSettings/Keys/BetaFeaturesCatalogSection.swift @@ -6,6 +6,16 @@ import Foundation /// pattern `.beta.` so the cmux.json view /// groups them sensibly. public struct BetaFeaturesCatalogSection: SettingCatalogSection { + /// Right-sidebar Feed: an experimental mode that surfaces inline agent + /// decisions (permission prompts, questions) in the right-sidebar mode + /// switcher. Defaults off; while off, the Feed mode is hidden from the + /// switcher so the feature stays opt-in while it is in beta. + public let rightSidebarFeed = DefaultsKey( + id: "rightSidebar.beta.feed.enabled", + defaultValue: false, + userDefaultsKey: "rightSidebar.beta.feed.enabled" + ) + /// Right-sidebar Dock: an experimental terminal-controls dock that /// replaces the per-pane action chrome with a unified right-side /// rail. Defaults off; flagged as unstable in the Settings UI. diff --git a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift index 3b024cc26743..a8d87082f84d 100644 --- a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift +++ b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift @@ -96,6 +96,7 @@ extension Array where Element == CuratedSettingEntry { .init(section: .sidebarAppearance, id: "show-metadata", title: "Show Custom Metadata in Sidebar", synonyms: "sidebar.showCustomMetadata metadata meta report_meta status custom block"), // Beta + .init(section: .betaFeatures, id: "feed", title: "Feed", synonyms: "feed right sidebar agent decisions permissions questions approval beta unstable"), .init(section: .betaFeatures, id: "dock", title: "Dock", synonyms: "dock right sidebar terminal controls tui beta unstable"), // Automation diff --git a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/SettingsSectionID.swift b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/SettingsSectionID.swift index 2c5b28351c37..191cb96ee727 100644 --- a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/SettingsSectionID.swift +++ b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/SettingsSectionID.swift @@ -77,7 +77,7 @@ public enum SettingsSectionID: String, CaseIterable, Identifiable, Sendable, Has case .terminal: return "scrollbar copy on select agent resume hibernation" case .textBox: return "textbox text box rich input prompt default new terminal workspace split tab focus show beta" case .sidebarAppearance: return "sidebar details branches material terminal background" - case .betaFeatures: return "beta experimental unstable dock right sidebar" + case .betaFeatures: return "beta experimental unstable feed dock right sidebar" case .automation: return "socket integrations hooks ports claude cursor gemini" case .browser: return "search engine links history theme" case .browserImport: return "browser import bookmarks history cookies" diff --git a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BetaFeaturesSection.swift b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BetaFeaturesSection.swift index ae119a4c7c60..b35534356607 100644 --- a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BetaFeaturesSection.swift +++ b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BetaFeaturesSection.swift @@ -2,14 +2,16 @@ import CmuxSettings import SwiftUI /// **Beta Features** section — a warning note followed by the -/// experimental toggles: `Dock` and `Extensions`. Each toggle gates an -/// unstable feature that is off by default. +/// experimental toggles: `Feed`, `Dock`, and `Extensions`. Each toggle +/// gates an unstable feature that is off by default. @MainActor public struct BetaFeaturesSection: View { + @State private var feed: DefaultsValueModel @State private var dock: DefaultsValueModel @State private var extensions: DefaultsValueModel public init(defaultsStore: UserDefaultsSettingsStore, catalog: SettingCatalog) { + _feed = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.rightSidebarFeed)) _dock = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.rightSidebarDock)) _extensions = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.extensions)) } @@ -22,6 +24,8 @@ public struct BetaFeaturesSection: View { String(localized: "settings.betaFeatures.warning", defaultValue: "These features are experimental and may change or break. Enable them only when you are testing them.") ) SettingsCardDivider() + feedRow + SettingsCardDivider() dockRow SettingsCardDivider() extensionsRow @@ -29,6 +33,23 @@ public struct BetaFeaturesSection: View { } } + @ViewBuilder + private var feedRow: some View { + SettingsCardRow( + configurationReview: .settingsOnly, + searchAnchorID: "setting:betaFeatures:feed", + String(localized: "settings.betaFeatures.feed", defaultValue: "Feed"), + subtitle: feed.current + ? String(localized: "settings.betaFeatures.feed.subtitleOn", defaultValue: "Shows Feed in the right sidebar mode switcher for inline agent decisions.") + : String(localized: "settings.betaFeatures.feed.subtitleOff", defaultValue: "Hides Feed from the right sidebar until you enable it here.") + ) { + Toggle("", isOn: Binding(get: { feed.current }, set: { feed.set($0) })) + .labelsHidden() + .controlSize(.small) + .accessibilityIdentifier("SettingsBetaFeedToggle") + } + } + @ViewBuilder private var dockRow: some View { SettingsCardRow( diff --git a/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift b/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift index b45d574eb7dc..3d9b9dbea420 100644 --- a/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift +++ b/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift @@ -124,6 +124,7 @@ struct SettingsRowAnchorResolutionTests { "setting:app:terminal-config", "setting:app:desktop-notifications", "setting:account:account", + "setting:betaFeatures:feed", "setting:betaFeatures:dock", "setting:browser:history", "setting:browser:http-allowlist", diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index b9f4f9667b4d..576d63ec7605 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -97998,6 +97998,57 @@ } } }, + "settings.betaFeatures.feed": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Feed" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Feed" + } + } + } + }, + "settings.betaFeatures.feed.subtitleOff": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hides Feed from the right sidebar until you enable it here." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ここで有効にするまで、Feed を右サイドバーから隠します。" + } + } + } + }, + "settings.betaFeatures.feed.subtitleOn": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shows Feed in the right sidebar mode switcher for inline agent decisions." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Feed を右サイドバーのモード切り替えに表示し、エージェントの判断をインラインで扱えるようにします。" + } + } + } + }, "settings.betaFeatures.warning": { "extractionState": "manual", "localizations": { diff --git a/Sources/App/WorkspaceRuntimeSettings.swift b/Sources/App/WorkspaceRuntimeSettings.swift index 702c3ea38d2d..06d3132a4fab 100644 --- a/Sources/App/WorkspaceRuntimeSettings.swift +++ b/Sources/App/WorkspaceRuntimeSettings.swift @@ -376,10 +376,17 @@ enum AgentHibernationTrackingGate { } enum RightSidebarBetaFeatureSettings { + static let feedEnabledKey = "rightSidebar.beta.feed.enabled" static let dockEnabledKey = "rightSidebar.beta.dock.enabled" + static let defaultFeedEnabled = false static let defaultDockEnabled = false + nonisolated static func isFeedEnabled(defaults: UserDefaults = .standard) -> Bool { + guard defaults.object(forKey: feedEnabledKey) != nil else { return defaultFeedEnabled } + return defaults.bool(forKey: feedEnabledKey) + } + nonisolated static func isDockEnabled(defaults: UserDefaults = .standard) -> Bool { guard defaults.object(forKey: dockEnabledKey) != nil else { return defaultDockEnabled } return defaults.bool(forKey: dockEnabledKey) diff --git a/Sources/CommandPalette/CommandPaletteSettingsToggle.swift b/Sources/CommandPalette/CommandPaletteSettingsToggle.swift index f1b15425afab..de928ebf41a8 100644 --- a/Sources/CommandPalette/CommandPaletteSettingsToggle.swift +++ b/Sources/CommandPalette/CommandPaletteSettingsToggle.swift @@ -661,6 +661,17 @@ enum CommandPaletteSettingsToggleCommands { defaultsKey: SidebarWorkspaceDetailDefaults.showCustomMetadataKey, isAvailable: sidebarDetailsAvailable ), + CommandPaletteSettingToggleDescriptor( + commandId: commandIdPrefix + "rightSidebarFeed", + settingsKey: "betaFeatures.feed", + title: { + String(localized: "settings.betaFeatures.feed", defaultValue: "Feed") + }, + sectionTitle: beta, + keywords: ["betaFeatures.feed", "feed", "right", "sidebar", "beta", "agent", "decisions", "permissions"], + defaultValue: RightSidebarBetaFeatureSettings.defaultFeedEnabled, + defaultsKey: RightSidebarBetaFeatureSettings.feedEnabledKey + ), CommandPaletteSettingToggleDescriptor( commandId: commandIdPrefix + "rightSidebarDock", settingsKey: "betaFeatures.dock", diff --git a/Sources/RightSidebarMode+Availability.swift b/Sources/RightSidebarMode+Availability.swift index 7886818e2a9d..f7ed79aea599 100644 --- a/Sources/RightSidebarMode+Availability.swift +++ b/Sources/RightSidebarMode+Availability.swift @@ -19,21 +19,29 @@ extension RightSidebarMode { } static func availableModes(defaults: UserDefaults = .standard) -> [RightSidebarMode] { - availableModes(dockEnabled: RightSidebarBetaFeatureSettings.isDockEnabled(defaults: defaults)) + availableModes( + feedEnabled: RightSidebarBetaFeatureSettings.isFeedEnabled(defaults: defaults), + dockEnabled: RightSidebarBetaFeatureSettings.isDockEnabled(defaults: defaults) + ) } - static func availableModes(dockEnabled: Bool) -> [RightSidebarMode] { - allCases.filter { $0.isAvailable(dockEnabled: dockEnabled) } + static func availableModes(feedEnabled: Bool, dockEnabled: Bool) -> [RightSidebarMode] { + allCases.filter { $0.isAvailable(feedEnabled: feedEnabled, dockEnabled: dockEnabled) } } func isAvailable(defaults: UserDefaults = .standard) -> Bool { - isAvailable(dockEnabled: RightSidebarBetaFeatureSettings.isDockEnabled(defaults: defaults)) + isAvailable( + feedEnabled: RightSidebarBetaFeatureSettings.isFeedEnabled(defaults: defaults), + dockEnabled: RightSidebarBetaFeatureSettings.isDockEnabled(defaults: defaults) + ) } - func isAvailable(dockEnabled: Bool) -> Bool { + func isAvailable(feedEnabled: Bool, dockEnabled: Bool) -> Bool { switch self { - case .files, .find, .sessions, .feed: + case .files, .find, .sessions: return true + case .feed: + return feedEnabled case .dock: return dockEnabled } diff --git a/Sources/RightSidebarPanelView.swift b/Sources/RightSidebarPanelView.swift index 9cbfe62b3d21..cd9ad7036eca 100644 --- a/Sources/RightSidebarPanelView.swift +++ b/Sources/RightSidebarPanelView.swift @@ -172,6 +172,8 @@ struct RightSidebarPanelView: View { private let closeShortcutHintYOffset = ShortcutHintDebugSettings.defaultRightSidebarCloseHintY private let focusShortcutHintXOffset = ShortcutHintDebugSettings.defaultRightSidebarFocusHintX private let focusShortcutHintYOffset = ShortcutHintDebugSettings.defaultRightSidebarFocusHintY + @AppStorage(RightSidebarBetaFeatureSettings.feedEnabledKey) + private var feedEnabled = RightSidebarBetaFeatureSettings.defaultFeedEnabled @AppStorage(RightSidebarBetaFeatureSettings.dockEnabledKey) private var dockEnabled = RightSidebarBetaFeatureSettings.defaultDockEnabled @@ -183,7 +185,7 @@ struct RightSidebarPanelView: View { } private var availableModes: [RightSidebarMode] { - RightSidebarMode.availableModes(dockEnabled: dockEnabled) + RightSidebarMode.availableModes(feedEnabled: feedEnabled, dockEnabled: dockEnabled) } var body: some View { @@ -223,6 +225,7 @@ struct RightSidebarPanelView: View { if mode != .dock { dockStore.deactivate() } } .onChange(of: fileExplorerState.isVisible) { _, visible in if !visible { dockStore.deactivate() } } + .onChange(of: feedEnabled) { _, _ in refreshModeAvailabilityAndFocusIfNeeded() } .onChange(of: dockEnabled) { _, _ in refreshModeAvailabilityAndFocusIfNeeded() } } diff --git a/Sources/SettingsNavigation.swift b/Sources/SettingsNavigation.swift index 6aad3f4aa837..eab2eca9c6bc 100644 --- a/Sources/SettingsNavigation.swift +++ b/Sources/SettingsNavigation.swift @@ -368,6 +368,7 @@ enum SettingsSearchIndex { setting(.sidebarAppearance, "show-log", String(localized: "settings.app.showLog", defaultValue: "Show Latest Log in Sidebar"), "status message"), setting(.sidebarAppearance, "show-progress", String(localized: "settings.app.showProgress", defaultValue: "Show Progress in Sidebar"), "progress bar"), setting(.sidebarAppearance, "show-metadata", String(localized: "settings.app.showMetadata", defaultValue: "Show Custom Metadata in Sidebar"), "report meta status block"), + setting(.betaFeatures, "feed", String(localized: "settings.betaFeatures.feed", defaultValue: "Feed"), "feed right sidebar agent decisions permissions questions"), setting(.betaFeatures, "dock", String(localized: "settings.betaFeatures.dock", defaultValue: "Dock"), "dock right sidebar terminal controls tui"), setting(.automation, "socket-mode", String(localized: "settings.automation.socketMode", defaultValue: "Socket Control Mode"), "unix socket api access password auth"), setting(.automation, "socket-password", String(localized: "settings.automation.socketPassword", defaultValue: "Socket Password"), "socket auth credential"), @@ -417,6 +418,7 @@ enum SettingsSearchIndex { ) private static let settingsPathAnchorIDs: [String: String] = [ + "rightSidebar.beta.feed.enabled": settingID(for: .betaFeatures, idSuffix: "feed"), "rightSidebar.beta.dock.enabled": settingID(for: .betaFeatures, idSuffix: "dock"), "app.language": settingID(for: .app, idSuffix: "language"), "app.appearance": settingID(for: .app, idSuffix: "appearance"), diff --git a/Sources/SettingsSearchAliases.swift b/Sources/SettingsSearchAliases.swift index 054cc13ab332..efce6e44cd5a 100644 --- a/Sources/SettingsSearchAliases.swift +++ b/Sources/SettingsSearchAliases.swift @@ -12,7 +12,7 @@ enum SettingsSearchAliasIndex { case .sidebarAppearance: return localized("settings.search.alias.section.sidebarAppearance", defaultValue: "sidebar left rail navigation details branches badges material terminal background") case .betaFeatures: - return localized("settings.search.alias.section.betaFeatures", defaultValue: "beta experimental unstable preview dock right sidebar") + return localized("settings.search.alias.section.betaFeatures", defaultValue: "beta experimental unstable preview feed dock right sidebar") case .automation: return localized("settings.search.alias.section.automation", defaultValue: "api cli control socket mcp agents hooks ports") case .browser: @@ -107,6 +107,7 @@ enum SettingsSearchAliasIndex { "sidebarAppearance:show-log": localized("settings.search.alias.setting.app.show-log", defaultValue: "sidebar.showLog log status latest message imperative"), "sidebarAppearance:show-progress": localized("settings.search.alias.setting.app.show-progress", defaultValue: "sidebar.showProgress progress bar percent status set_progress"), "sidebarAppearance:show-metadata": localized("settings.search.alias.setting.app.show-metadata", defaultValue: "sidebar.showCustomMetadata metadata meta report_meta status custom block"), + "betaFeatures:feed": localized("settings.search.alias.setting.betaFeatures.feed", defaultValue: "feed right sidebar agent decisions permissions questions approval beta unstable"), "betaFeatures:dock": localized("settings.search.alias.setting.betaFeatures.dock", defaultValue: "dock right sidebar terminal controls tui beta unstable"), "automation:socket-mode": localized("settings.search.alias.setting.automation.socket-mode", defaultValue: "automation.socketControlMode api socket unix domain control server auth allow password disabled"), "automation:socket-password": localized("settings.search.alias.setting.automation.socket-password", defaultValue: "automation.socketPassword auth token credential secret password access key"), diff --git a/cmuxTests/FileExplorerStateModePersistenceTests.swift b/cmuxTests/FileExplorerStateModePersistenceTests.swift index 9aab0ee65f40..cc76e9e7f650 100644 --- a/cmuxTests/FileExplorerStateModePersistenceTests.swift +++ b/cmuxTests/FileExplorerStateModePersistenceTests.swift @@ -9,12 +9,27 @@ import XCTest final class FileExplorerStateModePersistenceTests: XCTestCase { private let modeKey = "rightSidebar.mode" + private let feedEnabledKey = RightSidebarBetaFeatureSettings.feedEnabledKey private let dockEnabledKey = RightSidebarBetaFeatureSettings.dockEnabledKey - func testFeedStoredModeSurvivesByDefault() { + func testDisabledFeedStoredModeFallsBackToFiles() { withSavedRightSidebarModeDefaults { let defaults = UserDefaults.standard defaults.set(RightSidebarMode.feed.rawValue, forKey: modeKey) + defaults.set(false, forKey: feedEnabledKey) + + let state = FileExplorerState() + + XCTAssertEqual(state.mode, .files) + XCTAssertEqual(defaults.string(forKey: modeKey), RightSidebarMode.files.rawValue) + } + } + + func testEnabledFeedStoredModeSurvives() { + withSavedRightSidebarModeDefaults { + let defaults = UserDefaults.standard + defaults.set(RightSidebarMode.feed.rawValue, forKey: modeKey) + defaults.set(true, forKey: feedEnabledKey) let state = FileExplorerState() @@ -26,12 +41,13 @@ final class FileExplorerStateModePersistenceTests: XCTestCase { func testModeSetterClampsUnavailableBetaModes() { withSavedRightSidebarModeDefaults { let defaults = UserDefaults.standard + defaults.set(false, forKey: feedEnabledKey) defaults.set(false, forKey: dockEnabledKey) let state = FileExplorerState() state.mode = .feed - XCTAssertEqual(state.mode, .feed) - XCTAssertEqual(defaults.string(forKey: modeKey), RightSidebarMode.feed.rawValue) + XCTAssertEqual(state.mode, .files) + XCTAssertEqual(defaults.string(forKey: modeKey), RightSidebarMode.files.rawValue) defaults.set(true, forKey: dockEnabledKey) state.mode = .dock @@ -59,9 +75,11 @@ final class FileExplorerStateModePersistenceTests: XCTestCase { private func withSavedRightSidebarModeDefaults(_ body: () -> Void) { let defaults = UserDefaults.standard let previousMode = defaults.object(forKey: modeKey) + let previousFeedEnabled = defaults.object(forKey: feedEnabledKey) let previousDockEnabled = defaults.object(forKey: dockEnabledKey) defer { restore(previousMode, forKey: modeKey) + restore(previousFeedEnabled, forKey: feedEnabledKey) restore(previousDockEnabled, forKey: dockEnabledKey) } body() diff --git a/cmuxTests/RightSidebarCommandPaletteTests.swift b/cmuxTests/RightSidebarCommandPaletteTests.swift index c98c8b991f2b..4ef05c72d364 100644 --- a/cmuxTests/RightSidebarCommandPaletteTests.swift +++ b/cmuxTests/RightSidebarCommandPaletteTests.swift @@ -11,6 +11,7 @@ final class RightSidebarCommandPaletteTests: XCTestCase { func testCommandPaletteIncludesDefaultRightSidebarModes() throws { try withSavedBetaFeatureDefaults { let defaults = UserDefaults.standard + defaults.removeObject(forKey: RightSidebarBetaFeatureSettings.feedEnabledKey) defaults.removeObject(forKey: RightSidebarBetaFeatureSettings.dockEnabledKey) let contributions = ContentView.commandPaletteRightSidebarModeCommandContributions() let contributionsByID = Dictionary(uniqueKeysWithValues: contributions.map { ($0.commandId, $0) }) @@ -35,8 +36,8 @@ final class RightSidebarCommandPaletteTests: XCTestCase { XCTAssertTrue(contribution.enablement(context)) } - XCTAssertEqual(contributions.count, 4) - XCTAssertNotNil(contributionsByID[ContentView.commandPaletteRightSidebarModeCommandID(.feed)]) + XCTAssertEqual(contributions.count, 3) + XCTAssertNil(contributionsByID[ContentView.commandPaletteRightSidebarModeCommandID(.feed)]) XCTAssertNil(contributionsByID[ContentView.commandPaletteRightSidebarModeCommandID(.dock)]) } } @@ -44,6 +45,7 @@ final class RightSidebarCommandPaletteTests: XCTestCase { func testCommandPaletteRightSidebarActionsUseModeShortcutActions() { withSavedBetaFeatureDefaults { let defaults = UserDefaults.standard + defaults.set(true, forKey: RightSidebarBetaFeatureSettings.feedEnabledKey) defaults.set(true, forKey: RightSidebarBetaFeatureSettings.dockEnabledKey) for mode in RightSidebarMode.allCases { @@ -70,8 +72,10 @@ final class RightSidebarCommandPaletteTests: XCTestCase { private func withSavedBetaFeatureDefaults(_ body: () throws -> Void) rethrows { let defaults = UserDefaults.standard + let previousFeed = defaults.object(forKey: RightSidebarBetaFeatureSettings.feedEnabledKey) let previousDock = defaults.object(forKey: RightSidebarBetaFeatureSettings.dockEnabledKey) defer { + restore(previousFeed, forKey: RightSidebarBetaFeatureSettings.feedEnabledKey) restore(previousDock, forKey: RightSidebarBetaFeatureSettings.dockEnabledKey) } try body() diff --git a/cmuxUITests/RightSidebarChromeHeightUITests.swift b/cmuxUITests/RightSidebarChromeHeightUITests.swift index 9c97d0f3d880..d06432b6f620 100644 --- a/cmuxUITests/RightSidebarChromeHeightUITests.swift +++ b/cmuxUITests/RightSidebarChromeHeightUITests.swift @@ -14,6 +14,7 @@ final class RightSidebarChromeHeightUITests: XCTestCase { app.launchEnvironment["CMUX_UI_TEST_BONSPLIT_TAB_DRAG_PATH"] = dataPath app.launchEnvironment["CMUX_UI_TEST_BONSPLIT_SHOW_RIGHT_SIDEBAR"] = "1" app.launchArguments += ["-workspacePresentationMode", "minimal"] + app.launchArguments += ["-rightSidebar.beta.feed.enabled", "YES"] app.launch() defer { app.terminate() } diff --git a/cmuxUITests/SettingsSidebarBetaBehaviorUITests.swift b/cmuxUITests/SettingsSidebarBetaBehaviorUITests.swift index b9d4f420954b..e09a83172425 100644 --- a/cmuxUITests/SettingsSidebarBetaBehaviorUITests.swift +++ b/cmuxUITests/SettingsSidebarBetaBehaviorUITests.swift @@ -1,9 +1,9 @@ import XCTest /// Behavioral UI tests for the Settings **Sidebar** + **Beta Features** -/// section, scoped to the three controls called out for this section: +/// section, scoped to the controls called out for this section: /// the *Sidebar Branch Layout* picker (vertical vs inline), the active-tab -/// *indicator style*, and the *beta Dock* toggle. +/// *indicator style*, and the *beta Feed* / *beta Dock* toggles. /// /// What is actually assertable through XCUITest here, and why: /// @@ -38,10 +38,12 @@ final class SettingsSidebarBetaBehaviorUITests: SettingsUITestCase { // test so the run starts from the shipped default. // - sidebarBranchVerticalLayout: SidebarCatalogSection.branchVerticalLayout (default true / "Vertical") // - sidebarActiveTabIndicatorStyle: indicator style key (default "leftRail") + // - rightSidebar.beta.feed.enabled: BetaFeaturesCatalogSection.rightSidebarFeed (default false) // - rightSidebar.beta.dock.enabled: BetaFeaturesCatalogSection.rightSidebarDock (default false) private let inScopeDefaultsKeys = [ "sidebarBranchVerticalLayout", "sidebarActiveTabIndicatorStyle", + "rightSidebar.beta.feed.enabled", "rightSidebar.beta.dock.enabled", ] @@ -50,8 +52,13 @@ final class SettingsSidebarBetaBehaviorUITests: SettingsUITestCase { private let branchVerticalSubtitle = "Vertical: each branch appears on its own line." private let branchInlineSubtitle = "Inline: all branches share one line." + // Beta Feed subtitle strings (exact defaultValue copy from + // BetaFeaturesSection.feedRow). + private let feedOffSubtitle = "Hides Feed from the right sidebar until you enable it here." + private let feedOnSubtitle = "Shows Feed in the right sidebar mode switcher for inline agent decisions." + // Beta Dock subtitle strings (exact defaultValue copy from - // BetaFeaturesSettingsView.dockSubtitle). + // BetaFeaturesSection.dockRow). private let dockOffSubtitle = "Hides Dock from the right sidebar until you enable it here." private let dockOnSubtitle = "Shows Dock in the right sidebar mode switcher for custom terminal controls." @@ -125,6 +132,49 @@ final class SettingsSidebarBetaBehaviorUITests: SettingsUITestCase { ) } + // MARK: - TIER 1: Beta Feed toggle + + /// Toggling the **Beta Features → Feed** switch flips the row's derived + /// subtitle between the "Hides Feed …" (off) and "Shows Feed …" (on) + /// copy. The subtitle is computed from the `rightSidebarFeed` binding, + /// so the label change verifies the toggle drove the live settings store + /// and the dependent view re-rendered. + func testBetaFeedToggleDrivesDerivedSubtitle() { + let app = makeLaunchedApp() + let window = openSettings(app) + defer { closeSettings(app, window) } + + navigate(window, to: "Beta Features") + + let offSubtitle = window.staticTexts[feedOffSubtitle] + let onSubtitle = window.staticTexts[feedOnSubtitle] + + // Default is off → "Hides Feed …" present, "Shows Feed …" absent. + XCTAssertTrue( + poll(timeout: 5.0) { offSubtitle.exists }, + "Expected the default (off) Feed subtitle" + ) + XCTAssertFalse(onSubtitle.exists, "On subtitle should not be shown while Feed is disabled") + + let feedToggle = toggle(window, id: "SettingsBetaFeedToggle") + feedToggle.click() + + // Effect: subtitle flips to the "on" copy. + XCTAssertTrue( + poll(timeout: 5.0) { onSubtitle.exists }, + "Expected the (on) Feed subtitle after enabling Feed" + ) + XCTAssertFalse(offSubtitle.exists, "Off subtitle should be gone once Feed is enabled") + + // Toggle back off to prove the binding is reversible (full round-trip). + feedToggle.click() + XCTAssertTrue( + poll(timeout: 5.0) { offSubtitle.exists }, + "Expected the (off) Feed subtitle after disabling Feed again" + ) + XCTAssertFalse(onSubtitle.exists, "On subtitle should be gone once Feed is disabled again") + } + // MARK: - TIER 1: Beta Dock toggle /// Toggling the **Beta Features → Dock** switch flips the row's derived