Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ import Foundation
/// pattern `<feature-domain>.beta.<flag-name>` 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<Bool>(
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
austinywang marked this conversation as resolved.
@MainActor
public struct BetaFeaturesSection: View {
@State private var feed: DefaultsValueModel<Bool>
@State private var dock: DefaultsValueModel<Bool>
@State private var extensions: DefaultsValueModel<Bool>

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))
}
Expand All @@ -22,13 +24,32 @@ 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
}
}
}

@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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
51 changes: 51 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -98168,6 +98168,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": {
Expand Down
7 changes: 7 additions & 0 deletions Sources/App/WorkspaceRuntimeSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions Sources/CommandPalette/CommandPaletteSettingsToggle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 14 additions & 6 deletions Sources/RightSidebarMode+Availability.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/RightSidebarPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 {
Expand Down Expand Up @@ -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() }
}

Expand Down
2 changes: 2 additions & 0 deletions Sources/SettingsNavigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,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"),
Expand Down Expand Up @@ -418,6 +419,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"),
Expand Down
3 changes: 2 additions & 1 deletion Sources/SettingsSearchAliases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment thread
austinywang marked this conversation as resolved.
case .automation:
return localized("settings.search.alias.section.automation", defaultValue: "api cli control socket mcp agents hooks ports")
case .browser:
Expand Down Expand Up @@ -108,6 +108,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"),
Comment thread
austinywang marked this conversation as resolved.
"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"),
Expand Down
24 changes: 21 additions & 3 deletions cmuxTests/FileExplorerStateModePersistenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 6 additions & 2 deletions cmuxTests/RightSidebarCommandPaletteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) })
Expand All @@ -35,15 +36,16 @@ 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)])
}
}

func testCommandPaletteRightSidebarActionsUseModeShortcutActions() {
withSavedBetaFeatureDefaults {
let defaults = UserDefaults.standard
defaults.set(true, forKey: RightSidebarBetaFeatureSettings.feedEnabledKey)
defaults.set(true, forKey: RightSidebarBetaFeatureSettings.dockEnabledKey)

for mode in RightSidebarMode.allCases {
Expand All @@ -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()
Expand Down
Loading
Loading