diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings
index db674105e604..4030c1062b22 100644
--- a/Resources/Localizable.xcstrings
+++ b/Resources/Localizable.xcstrings
@@ -10037,6 +10037,74 @@
}
}
},
+ "command.rightSidebarShow.title": {
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Show Right Sidebar"
+ }
+ },
+ "ja": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "右サイドバーを表示"
+ }
+ }
+ }
+ },
+ "command.rightSidebarHide.title": {
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Hide Right Sidebar"
+ }
+ },
+ "ja": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "右サイドバーを非表示"
+ }
+ }
+ }
+ },
+ "command.rightSidebarFocus.title": {
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Focus Right Sidebar"
+ }
+ },
+ "ja": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "右サイドバーにフォーカス"
+ }
+ }
+ }
+ },
+ "command.rightSidebarSessions.title": {
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Show Sidebar Sessions"
+ }
+ },
+ "ja": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "サイドバーのセッションを表示"
+ }
+ }
+ }
+ },
"command.openRightSidebarToolAsPane.subtitle": {
"extractionState": "manual",
"localizations": {
@@ -108280,6 +108348,23 @@
}
}
},
+ "shortcut.switchRightSidebarToVault.label": {
+ "extractionState": "manual",
+ "localizations": {
+ "en": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "Show Sidebar Vault"
+ }
+ },
+ "ja": {
+ "stringUnit": {
+ "state": "translated",
+ "value": "サイドバーのボールトを表示"
+ }
+ }
+ }
+ },
"shortcut.switchRightSidebarToSessions.label": {
"extractionState": "manual",
"localizations": {
diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift
index 438df7b3d609..feb10b5d65dd 100644
--- a/Sources/AppDelegate.swift
+++ b/Sources/AppDelegate.swift
@@ -5873,9 +5873,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
func applyRightSidebarRemoteCommand(
_ command: RightSidebarRemoteCommand,
- target: RightSidebarRemoteTarget = RightSidebarRemoteTarget()
+ target: RightSidebarRemoteTarget = RightSidebarRemoteTarget(),
+ preferredWindow: NSWindow? = nil
) -> RightSidebarRemoteApplyResult {
- let context = rightSidebarRemoteContext(target: target)
+ let context = rightSidebarRemoteContext(target: target, preferredWindow: preferredWindow)
if !target.isActiveTarget, context == nil {
return .failure(String(localized: "rightSidebar.remote.error.targetNotFound", defaultValue: "ERROR: Right sidebar target not found"))
}
@@ -5961,7 +5962,156 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
}
- private func rightSidebarRemoteContext(target: RightSidebarRemoteTarget) -> MainWindowContext? {
+ @discardableResult
+ func executeBuiltInRightSidebarAction(
+ _ action: CmuxSurfaceTabBarBuiltInAction,
+ preferredWindow: NSWindow? = nil,
+ onExecuted: (() -> Void)? = nil
+ ) -> Bool {
+ executeBuiltInAction(
+ action,
+ scope: .rightSidebarOnly,
+ preferredWindow: preferredWindow,
+ onExecuted: onExecuted
+ )
+ }
+
+ @discardableResult
+ func executeSurfaceTabBarBuiltInAction(
+ _ action: CmuxSurfaceTabBarBuiltInAction,
+ tabManager: TabManager?,
+ preferredWindow: NSWindow? = nil
+ ) -> Bool {
+ executeBuiltInAction(
+ action,
+ scope: .surfaceTabBar(tabManager),
+ preferredWindow: preferredWindow
+ )
+ }
+
+ private enum BuiltInActionExecutionScope {
+ case configured(MainWindowContext)
+ case surfaceTabBar(TabManager?)
+ case rightSidebarOnly
+ }
+
+ @discardableResult
+ private func executeBuiltInAction(
+ _ action: CmuxSurfaceTabBarBuiltInAction,
+ scope: BuiltInActionExecutionScope,
+ preferredWindow: NSWindow? = nil,
+ onExecuted: (() -> Void)? = nil
+ ) -> Bool {
+ switch action {
+ case .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
+ .rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
+ .rightSidebarFeed, .rightSidebarDock:
+ guard let command = action.rightSidebarRemoteCommand else {
+ return false
+ }
+ switch applyRightSidebarRemoteCommand(command, preferredWindow: preferredWindow) {
+ case .ok:
+ onExecuted?()
+ return true
+ case .state, .failure:
+ return false
+ }
+
+ case .newWorkspace:
+ switch scope {
+ case .configured(let context):
+ context.tabManager.addWorkspace()
+ onExecuted?()
+ return true
+ case .surfaceTabBar(let tabManager):
+ guard let tabManager else { return false }
+ tabManager.addWorkspace()
+ return true
+ case .rightSidebarOnly:
+ return false
+ }
+
+ case .cloudVM:
+ switch scope {
+ case .configured(let context):
+ let didStart = performCloudVMAction(
+ tabManager: context.tabManager,
+ preferredWindow: resolvedWindow(for: context) ?? preferredWindow,
+ debugSource: "configured.cmux.cloudvm"
+ )
+ if didStart { onExecuted?() }
+ return didStart
+ case .surfaceTabBar(let tabManager):
+ return performCloudVMAction(
+ tabManager: tabManager,
+ preferredWindow: preferredWindow,
+ debugSource: "surfaceTabBar.cloudVM"
+ )
+ case .rightSidebarOnly:
+ return false
+ }
+
+ case .newTerminal:
+ guard case .configured(let context) = scope else {
+ return false
+ }
+ context.tabManager.newSurface()
+ onExecuted?()
+ return true
+
+ case .newBrowser:
+ guard case .configured(let context) = scope else {
+ return false
+ }
+ let previousTabManager = tabManager
+ tabManager = context.tabManager
+ defer { tabManager = previousTabManager }
+ guard openBrowserAndFocusAddressBar(insertAtEnd: true) != nil else {
+ return false
+ }
+ onExecuted?()
+ return true
+
+ case .splitRight:
+ guard case .configured(let context) = scope else {
+ return false
+ }
+ if shouldSuppressSplitShortcutForTransientTerminalFocusState(
+ direction: .right,
+ tabManager: context.tabManager
+ ) {
+ return true
+ }
+ let didSplit = performSplitShortcut(
+ direction: .right,
+ preferredWindow: preferredWindow ?? NSApp.keyWindow ?? NSApp.mainWindow
+ )
+ if didSplit { onExecuted?() }
+ return didSplit
+
+ case .splitDown:
+ guard case .configured(let context) = scope else {
+ return false
+ }
+ if shouldSuppressSplitShortcutForTransientTerminalFocusState(
+ direction: .down,
+ tabManager: context.tabManager
+ ) {
+ return true
+ }
+ let didSplit = performSplitShortcut(
+ direction: .down,
+ preferredWindow: preferredWindow ?? NSApp.keyWindow ?? NSApp.mainWindow
+ )
+ if didSplit { onExecuted?() }
+ return didSplit
+ }
+ }
+
+ private func rightSidebarRemoteContext(
+ target: RightSidebarRemoteTarget,
+ preferredWindow: NSWindow? = nil
+ ) -> MainWindowContext? {
if let windowId = target.windowId {
return mainWindowContexts.values.first(where: { $0.windowId == windowId })
}
@@ -5970,7 +6120,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
context.tabManager.tabs.contains(where: { $0.id == workspaceId })
}
}
- return preferredRegisteredMainWindowContext()
+ return preferredRegisteredMainWindowContext(preferredWindow: preferredWindow)
}
@discardableResult
@@ -13641,59 +13791,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
) -> Bool {
switch action.action {
case .builtIn(let builtIn):
- switch builtIn {
- case .newWorkspace:
- context.tabManager.addWorkspace()
- onExecuted?()
- return true
- case .cloudVM:
- let didStart = performCloudVMAction(
- tabManager: context.tabManager,
- preferredWindow: resolvedWindow(for: context) ?? preferredWindow,
- debugSource: "configured.cmux.cloudvm"
- )
- if didStart { onExecuted?() }
- return didStart
- case .newTerminal:
- context.tabManager.newSurface()
- onExecuted?()
- return true
- case .newBrowser:
- let previousTabManager = tabManager
- tabManager = context.tabManager
- defer { tabManager = previousTabManager }
- guard openBrowserAndFocusAddressBar(insertAtEnd: true) != nil else {
- return false
- }
- onExecuted?()
- return true
- case .splitRight:
- if shouldSuppressSplitShortcutForTransientTerminalFocusState(
- direction: .right,
- tabManager: context.tabManager
- ) {
- return true
- }
- let didSplit = performSplitShortcut(
- direction: .right,
- preferredWindow: preferredWindow ?? NSApp.keyWindow ?? NSApp.mainWindow
- )
- if didSplit { onExecuted?() }
- return didSplit
- case .splitDown:
- if shouldSuppressSplitShortcutForTransientTerminalFocusState(
- direction: .down,
- tabManager: context.tabManager
- ) {
- return true
- }
- let didSplit = performSplitShortcut(
- direction: .down,
- preferredWindow: preferredWindow ?? NSApp.keyWindow ?? NSApp.mainWindow
- )
- if didSplit { onExecuted?() }
- return didSplit
- }
+ return executeBuiltInAction(
+ builtIn,
+ scope: .configured(context),
+ preferredWindow: preferredWindow,
+ onExecuted: onExecuted
+ )
case .command, .agent, .workspaceCommand:
guard let cmuxConfigStore = context.cmuxConfigStore else {
return false
diff --git a/Sources/CmuxConfig.swift b/Sources/CmuxConfig.swift
index 4d47710ae764..c195c1f1af76 100644
--- a/Sources/CmuxConfig.swift
+++ b/Sources/CmuxConfig.swift
@@ -1499,34 +1499,11 @@ struct CmuxResolvedConfigAction: Identifiable, Sendable, Hashable {
}
static func builtIn(_ builtIn: CmuxSurfaceTabBarBuiltInAction) -> CmuxResolvedConfigAction {
- let title: String
- let keywords: [String]
- switch builtIn {
- case .newWorkspace:
- title = String(localized: "command.newWorkspace.title", defaultValue: "New Workspace")
- keywords = ["create", "new", "workspace"]
- case .cloudVM:
- title = String(localized: "command.cloudVM.title", defaultValue: "Start Cloud VM")
- keywords = ["cloud", "vm", "virtual", "machine", "remote"]
- case .newTerminal:
- title = String(localized: "command.newTerminalTab.title", defaultValue: "New Terminal Tab")
- keywords = ["new", "terminal", "tab", "surface"]
- case .newBrowser:
- title = String(localized: "command.newBrowserTab.title", defaultValue: "New Browser Tab")
- keywords = ["new", "browser", "tab", "surface"]
- case .splitRight:
- title = String(localized: "command.terminalSplitRight.title", defaultValue: "Split Right")
- keywords = ["terminal", "split", "right"]
- case .splitDown:
- title = String(localized: "command.terminalSplitDown.title", defaultValue: "Split Down")
- keywords = ["terminal", "split", "down"]
- }
-
return CmuxResolvedConfigAction(
id: builtIn.configID,
- title: title,
+ title: builtIn.defaultTitle,
subtitle: String(localized: "command.cmuxConfig.builtInSubtitle", defaultValue: "cmux"),
- keywords: keywords,
+ keywords: builtIn.defaultKeywords,
palette: true,
shortcut: nil,
icon: .symbol(builtIn.defaultIcon),
diff --git a/Sources/CmuxSurfaceTabBarBuiltInAction.swift b/Sources/CmuxSurfaceTabBarBuiltInAction.swift
index cc7586e14ea8..aa471310cb94 100644
--- a/Sources/CmuxSurfaceTabBarBuiltInAction.swift
+++ b/Sources/CmuxSurfaceTabBarBuiltInAction.swift
@@ -8,6 +8,20 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha
case newBrowser = "cmux.newBrowser"
case splitRight = "cmux.splitRight"
case splitDown = "cmux.splitDown"
+ case rightSidebarToggle = "cmux.rightSidebar.toggle"
+ case rightSidebarShow = "cmux.rightSidebar.show"
+ case rightSidebarHide = "cmux.rightSidebar.hide"
+ case rightSidebarFocus = "cmux.rightSidebar.focus"
+ case rightSidebarFiles = "cmux.rightSidebar.files"
+ case rightSidebarFind = "cmux.rightSidebar.find"
+ case rightSidebarVault = "cmux.rightSidebar.vault"
+ case rightSidebarSessions = "cmux.rightSidebar.sessions"
+ case rightSidebarFeed = "cmux.rightSidebar.feed"
+ case rightSidebarDock = "cmux.rightSidebar.dock"
+
+ static let rightSidebarActions: [CmuxSurfaceTabBarBuiltInAction] = allCases.filter {
+ $0.rightSidebarRemoteCommand != nil
+ }
init?(configID: String) {
switch configID {
@@ -25,6 +39,26 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha
self = .splitRight
case "cmux.splitDown", "splitDown":
self = .splitDown
+ case "cmux.rightSidebar.toggle", "rightSidebar.toggle":
+ self = .rightSidebarToggle
+ case "cmux.rightSidebar.show", "rightSidebar.show":
+ self = .rightSidebarShow
+ case "cmux.rightSidebar.hide", "rightSidebar.hide":
+ self = .rightSidebarHide
+ case "cmux.rightSidebar.focus", "rightSidebar.focus":
+ self = .rightSidebarFocus
+ case "cmux.rightSidebar.files", "rightSidebar.files":
+ self = .rightSidebarFiles
+ case "cmux.rightSidebar.find", "rightSidebar.find":
+ self = .rightSidebarFind
+ case "cmux.rightSidebar.vault", "rightSidebar.vault":
+ self = .rightSidebarVault
+ case "cmux.rightSidebar.sessions", "rightSidebar.sessions":
+ self = .rightSidebarSessions
+ case "cmux.rightSidebar.feed", "rightSidebar.feed":
+ self = .rightSidebarFeed
+ case "cmux.rightSidebar.dock", "rightSidebar.dock":
+ self = .rightSidebarDock
default:
return nil
}
@@ -48,12 +82,128 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha
return "square.split.2x1"
case .splitDown:
return "square.split.1x2"
+ case .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide:
+ return "rectangle.righthalf.inset.filled"
+ case .rightSidebarFocus:
+ return "scope"
+ case .rightSidebarFiles:
+ return RightSidebarMode.files.symbolName
+ case .rightSidebarFind:
+ return RightSidebarMode.find.symbolName
+ case .rightSidebarVault, .rightSidebarSessions:
+ return RightSidebarMode.sessions.symbolName
+ case .rightSidebarFeed:
+ return RightSidebarMode.feed.symbolName
+ case .rightSidebarDock:
+ return RightSidebarMode.dock.symbolName
+ }
+ }
+
+ var defaultTitle: String {
+ switch self {
+ case .newWorkspace:
+ return String(localized: "command.newWorkspace.title", defaultValue: "New Workspace")
+ case .cloudVM:
+ return String(localized: "command.cloudVM.title", defaultValue: "Start Cloud VM")
+ case .newTerminal:
+ return String(localized: "command.newTerminalTab.title", defaultValue: "New Terminal Tab")
+ case .newBrowser:
+ return String(localized: "command.newBrowserTab.title", defaultValue: "New Browser Tab")
+ case .splitRight:
+ return String(localized: "command.terminalSplitRight.title", defaultValue: "Split Right")
+ case .splitDown:
+ return String(localized: "command.terminalSplitDown.title", defaultValue: "Split Down")
+ case .rightSidebarToggle:
+ return String(localized: "shortcut.toggleRightSidebar.label", defaultValue: "Toggle Right Sidebar")
+ case .rightSidebarShow:
+ return String(localized: "command.rightSidebarShow.title", defaultValue: "Show Right Sidebar")
+ case .rightSidebarHide:
+ return String(localized: "command.rightSidebarHide.title", defaultValue: "Hide Right Sidebar")
+ case .rightSidebarFocus:
+ return String(localized: "command.rightSidebarFocus.title", defaultValue: "Focus Right Sidebar")
+ case .rightSidebarFiles:
+ return String(localized: "shortcut.switchRightSidebarToFiles.label", defaultValue: "Show Sidebar Files")
+ case .rightSidebarFind:
+ return String(localized: "shortcut.switchRightSidebarToFind.label", defaultValue: "Show Sidebar Find")
+ case .rightSidebarVault:
+ return String(localized: "shortcut.switchRightSidebarToVault.label", defaultValue: "Show Sidebar Vault")
+ case .rightSidebarSessions:
+ return String(localized: "command.rightSidebarSessions.title", defaultValue: "Show Sidebar Sessions")
+ case .rightSidebarFeed:
+ return String(localized: "shortcut.switchRightSidebarToFeed.label", defaultValue: "Show Sidebar Feed")
+ case .rightSidebarDock:
+ return String(localized: "shortcut.switchRightSidebarToDock.label", defaultValue: "Show Sidebar Dock")
+ }
+ }
+
+ var defaultKeywords: [String] {
+ switch self {
+ case .newWorkspace:
+ return ["create", "new", "workspace"]
+ case .cloudVM:
+ return ["cloud", "vm", "virtual", "machine", "remote"]
+ case .newTerminal:
+ return ["new", "terminal", "tab", "surface"]
+ case .newBrowser:
+ return ["new", "browser", "tab", "surface"]
+ case .splitRight:
+ return ["terminal", "split", "right"]
+ case .splitDown:
+ return ["terminal", "split", "down"]
+ case .rightSidebarToggle:
+ return ["right", "sidebar", "toggle"]
+ case .rightSidebarShow:
+ return ["right", "sidebar", "show"]
+ case .rightSidebarHide:
+ return ["right", "sidebar", "hide"]
+ case .rightSidebarFocus:
+ return ["right", "sidebar", "focus"]
+ case .rightSidebarFiles:
+ return ["right", "sidebar", "files"]
+ case .rightSidebarFind:
+ return ["right", "sidebar", "find", "search"]
+ case .rightSidebarVault:
+ return ["right", "sidebar", "vault", "sessions"]
+ case .rightSidebarSessions:
+ return ["right", "sidebar", "sessions", "vault"]
+ case .rightSidebarFeed:
+ return ["right", "sidebar", "feed"]
+ case .rightSidebarDock:
+ return ["right", "sidebar", "dock"]
+ }
+ }
+
+ var rightSidebarRemoteCommand: RightSidebarRemoteCommand? {
+ switch self {
+ case .rightSidebarToggle:
+ return .toggle
+ case .rightSidebarShow:
+ return .show
+ case .rightSidebarHide:
+ return .hide
+ case .rightSidebarFocus:
+ return .focus
+ case .rightSidebarFiles:
+ return .setMode(.files, focus: true)
+ case .rightSidebarFind:
+ return .setMode(.find, focus: true)
+ case .rightSidebarVault, .rightSidebarSessions:
+ return .setMode(.sessions, focus: true)
+ case .rightSidebarFeed:
+ return .setMode(.feed, focus: true)
+ case .rightSidebarDock:
+ return .setMode(.dock, focus: true)
+ case .newWorkspace, .cloudVM, .newTerminal, .newBrowser, .splitRight, .splitDown:
+ return nil
}
}
var bonsplitAction: BonsplitConfiguration.SplitActionButton.Action? {
switch self {
- case .newWorkspace, .cloudVM:
+ case .newWorkspace, .cloudVM,
+ .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
+ .rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
+ .rightSidebarFeed, .rightSidebarDock:
return nil
case .newTerminal:
return .newTerminal
diff --git a/Sources/ContentView+RightSidebarCommandPalette.swift b/Sources/ContentView+RightSidebarCommandPalette.swift
index 8876c7e4c6b3..62d8e1e7f110 100644
--- a/Sources/ContentView+RightSidebarCommandPalette.swift
+++ b/Sources/ContentView+RightSidebarCommandPalette.swift
@@ -187,8 +187,12 @@ extension ContentView {
private static func commandPaletteRightSidebarModeShortcutAction(
forCommandID commandID: String
) -> KeyboardShortcutSettings.Action? {
+ commandPaletteRightSidebarMode(forCommandID: commandID)?.shortcutAction
+ }
+
+ static func commandPaletteRightSidebarMode(forCommandID commandID: String) -> RightSidebarMode? {
RightSidebarMode.availableModes().first { mode in
Self.commandPaletteRightSidebarModeCommandID(mode) == commandID
- }?.shortcutAction
+ }
}
}
diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift
index e7e58e8dc44f..0aa6b80e592d 100644
--- a/Sources/ContentView.swift
+++ b/Sources/ContentView.swift
@@ -6122,6 +6122,10 @@ struct ContentView: View {
}
private func commandPaletteConfigActionID(for commandId: String) -> String? {
+ if let rightSidebarMode = Self.commandPaletteRightSidebarMode(forCommandID: commandId) {
+ return rightSidebarMode.builtInAction.configID
+ }
+
switch commandId {
case "palette.newTerminalTab":
return CmuxSurfaceTabBarBuiltInAction.newTerminal.configID
diff --git a/Sources/RightSidebarPanelView.swift b/Sources/RightSidebarPanelView.swift
index b724ef2fcf00..4d6dcf6c061f 100644
--- a/Sources/RightSidebarPanelView.swift
+++ b/Sources/RightSidebarPanelView.swift
@@ -45,6 +45,16 @@ nonisolated enum RightSidebarMode: String, CaseIterable, Codable, Sendable {
case .dock: return .switchRightSidebarToDock
}
}
+
+ var builtInAction: CmuxSurfaceTabBarBuiltInAction {
+ switch self {
+ case .files: return .rightSidebarFiles
+ case .find: return .rightSidebarFind
+ case .sessions: return .rightSidebarSessions
+ case .feed: return .rightSidebarFeed
+ case .dock: return .rightSidebarDock
+ }
+ }
}
extension RightSidebarMode {
diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift
index 0959bc7a89f1..f7d20029af4c 100644
--- a/Sources/Workspace.swift
+++ b/Sources/Workspace.swift
@@ -14842,18 +14842,11 @@ extension Workspace: BonsplitDelegate {
?? NSApp.mainWindow
if let builtInAction = executable.builtInAction {
- switch builtInAction {
- case .newWorkspace:
- owningTabManager?.addWorkspace()
- case .cloudVM:
- _ = AppDelegate.shared?.performCloudVMAction(
- tabManager: owningTabManager,
- preferredWindow: presentingWindow,
- debugSource: "surfaceTabBar.cloudVM"
- )
- case .newTerminal, .newBrowser, .splitRight, .splitDown:
- break
- }
+ _ = AppDelegate.shared?.executeSurfaceTabBarBuiltInAction(
+ builtInAction,
+ tabManager: owningTabManager,
+ preferredWindow: presentingWindow
+ )
return
}
diff --git a/cmuxTests/CmuxConfigTests.swift b/cmuxTests/CmuxConfigTests.swift
index e61550d8b6e1..89537ee0fdf8 100644
--- a/cmuxTests/CmuxConfigTests.swift
+++ b/cmuxTests/CmuxConfigTests.swift
@@ -334,6 +334,90 @@ final class CmuxConfigDecodingTests: XCTestCase {
XCTAssertEqual(button.action, .command("codex"))
}
+ @MainActor
+ func testRightSidebarBuiltInActionsResolveFromRegistry() throws {
+ let root = FileManager.default.temporaryDirectory
+ .appendingPathComponent("cmux-config-right-sidebar-\(UUID().uuidString)", isDirectory: true)
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
+ defer { try? FileManager.default.removeItem(at: root) }
+
+ let globalConfigURL = root.appendingPathComponent("cmux.json")
+ try "{}".write(to: globalConfigURL, atomically: true, encoding: .utf8)
+
+ let store = CmuxConfigStore(
+ globalConfigPath: globalConfigURL.path,
+ startFileWatchers: false
+ )
+ store.loadAll()
+
+ for action in CmuxSurfaceTabBarBuiltInAction.rightSidebarActions {
+ let resolved = try XCTUnwrap(
+ store.resolvedAction(id: action.configID),
+ "Expected action registry entry for \(action.configID)"
+ )
+ XCTAssertEqual(resolved.id, action.configID)
+ XCTAssertEqual(resolved.action, .builtIn(action))
+ XCTAssertNil(resolved.terminalCommand)
+ XCTAssertEqual(resolved.icon, .symbol(action.defaultIcon))
+ }
+ }
+
+ @MainActor
+ func testRightSidebarBuiltInActionMetadataStaysCompleteAndDistinct() {
+ let expectedActions = CmuxSurfaceTabBarBuiltInAction.allCases.filter {
+ $0.rightSidebarRemoteCommand != nil
+ }
+ let actions = CmuxSurfaceTabBarBuiltInAction.rightSidebarActions
+
+ XCTAssertEqual(actions, expectedActions)
+ XCTAssertEqual(Set(actions.map(\.configID)).count, actions.count)
+ XCTAssertEqual(Set(actions.map(\.defaultTitle)).count, actions.count)
+
+ for action in actions {
+ XCTAssertFalse(action.defaultIcon.isEmpty)
+ XCTAssertFalse(action.defaultKeywords.isEmpty)
+ XCTAssertNotNil(action.rightSidebarRemoteCommand)
+ }
+ }
+
+ @MainActor
+ func testSurfaceTabBarCanResolveRightSidebarToggleBuiltInAction() throws {
+ let root = FileManager.default.temporaryDirectory
+ .appendingPathComponent("cmux-config-right-sidebar-button-\(UUID().uuidString)", isDirectory: true)
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
+ defer { try? FileManager.default.removeItem(at: root) }
+
+ let globalConfigURL = root.appendingPathComponent("cmux.json")
+ let json = """
+ {
+ "ui": {
+ "surfaceTabBar": {
+ "buttons": [
+ "cmux.newTerminal",
+ "cmux.splitRight",
+ "cmux.splitDown",
+ "cmux.rightSidebar.toggle"
+ ]
+ }
+ }
+ }
+ """
+ try json.write(to: globalConfigURL, atomically: true, encoding: .utf8)
+
+ let store = CmuxConfigStore(
+ globalConfigPath: globalConfigURL.path,
+ startFileWatchers: false
+ )
+ store.loadAll()
+
+ XCTAssertEqual(store.surfaceTabBarButtons.count, 4)
+ let button = store.surfaceTabBarButtons[3]
+ XCTAssertEqual(button.id, CmuxSurfaceTabBarBuiltInAction.rightSidebarToggle.configID)
+ XCTAssertEqual(button.action, .builtIn(.rightSidebarToggle))
+ XCTAssertNil(button.terminalCommand)
+ XCTAssertEqual(button.icon, .symbol(CmuxSurfaceTabBarBuiltInAction.rightSidebarToggle.defaultIcon))
+ }
+
@MainActor
func testSurfaceTabBarActionReferenceUsesActionSourcePath() throws {
let root = FileManager.default.temporaryDirectory
diff --git a/cmuxTests/TerminalControllerSocketSecurityTests.swift b/cmuxTests/TerminalControllerSocketSecurityTests.swift
index f5d00e6fa7fd..7dd6468003a4 100644
--- a/cmuxTests/TerminalControllerSocketSecurityTests.swift
+++ b/cmuxTests/TerminalControllerSocketSecurityTests.swift
@@ -451,6 +451,138 @@ final class TerminalControllerSocketSecurityTests: XCTestCase {
}
}
+ func testRightSidebarBuiltInActionsMatchCLISubcommandEffects() throws {
+ try withSavedRightSidebarDockAvailability {
+ UserDefaults.standard.set(true, forKey: RightSidebarBetaFeatureSettings.dockEnabledKey)
+
+ let cases: [(CmuxSurfaceTabBarBuiltInAction, [String], Bool, RightSidebarMode)] = [
+ (.rightSidebarToggle, ["toggle"], false, .files),
+ (.rightSidebarShow, ["show"], false, .files),
+ (.rightSidebarHide, ["hide"], true, .files),
+ (.rightSidebarFocus, ["focus"], false, .find),
+ (.rightSidebarFiles, ["files"], false, .find),
+ (.rightSidebarFind, ["find"], false, .files),
+ (.rightSidebarVault, ["vault"], false, .files),
+ (.rightSidebarSessions, ["sessions"], false, .files),
+ (.rightSidebarFeed, ["feed"], false, .files),
+ (.rightSidebarDock, ["dock"], false, .files)
+ ]
+
+ for (action, cliTokens, initialVisible, initialMode) in cases {
+ let cliState = try rightSidebarStateAfterCLI(
+ tokens: cliTokens,
+ initialVisible: initialVisible,
+ initialMode: initialMode
+ )
+ let actionState = try rightSidebarStateAfterBuiltInAction(
+ action,
+ initialVisible: initialVisible,
+ initialMode: initialMode
+ )
+
+ XCTAssertEqual(actionState.succeeded, cliState.succeeded, action.configID)
+ XCTAssertEqual(actionState.visible, cliState.visible, action.configID)
+ XCTAssertEqual(actionState.mode, cliState.mode, action.configID)
+ }
+ }
+ }
+
+ func testRightSidebarToggleBuiltInMatchesKeyboardShortcutToggleEffect() throws {
+ let shortcutState = try rightSidebarStateAfter(
+ initialVisible: false,
+ initialMode: .files
+ ) { appDelegate in
+ appDelegate.toggleRightSidebarInActiveMainWindow()
+ }
+ let actionState = try rightSidebarStateAfterBuiltInAction(
+ .rightSidebarToggle,
+ initialVisible: false,
+ initialMode: .files
+ )
+
+ XCTAssertEqual(actionState.succeeded, shortcutState.succeeded)
+ XCTAssertEqual(actionState.visible, shortcutState.visible)
+ XCTAssertEqual(actionState.mode, shortcutState.mode)
+ }
+
+ private func rightSidebarStateAfterCLI(
+ tokens: [String],
+ initialVisible: Bool,
+ initialMode: RightSidebarMode
+ ) throws -> (succeeded: Bool, visible: Bool, mode: RightSidebarMode) {
+ let request: RightSidebarRemoteRequest
+ switch RightSidebarRemoteRequest.parse(tokens: tokens) {
+ case .success(let parsed):
+ request = parsed
+ case .failure(let error):
+ XCTFail("Expected CLI tokens to parse: \(tokens.joined(separator: " ")) \(error.message)")
+ return (false, initialVisible, initialMode)
+ }
+
+ return try rightSidebarStateAfter(
+ initialVisible: initialVisible,
+ initialMode: initialMode
+ ) { appDelegate in
+ switch appDelegate.applyRightSidebarRemoteCommand(request.command, target: request.target) {
+ case .ok:
+ return true
+ case .state, .failure:
+ return false
+ }
+ }
+ }
+
+ private func rightSidebarStateAfterBuiltInAction(
+ _ action: CmuxSurfaceTabBarBuiltInAction,
+ initialVisible: Bool,
+ initialMode: RightSidebarMode
+ ) throws -> (succeeded: Bool, visible: Bool, mode: RightSidebarMode) {
+ try rightSidebarStateAfter(
+ initialVisible: initialVisible,
+ initialMode: initialMode
+ ) { appDelegate in
+ appDelegate.executeBuiltInRightSidebarAction(action)
+ }
+ }
+
+ private func rightSidebarStateAfter(
+ initialVisible: Bool,
+ initialMode: RightSidebarMode,
+ apply: (AppDelegate) -> Bool
+ ) throws -> (succeeded: Bool, visible: Bool, mode: RightSidebarMode) {
+ let previousAppDelegate = AppDelegate.shared
+ let appDelegate = AppDelegate()
+ defer { AppDelegate.shared = previousAppDelegate }
+
+ let manager = TabManager()
+ _ = manager.addWorkspace(select: false, eagerLoadTerminal: false)
+ let state = FileExplorerState()
+ state.setVisible(initialVisible)
+ state.mode = initialMode
+
+ let windowId = appDelegate.registerMainWindowContextForTesting(
+ tabManager: manager,
+ fileExplorerState: state
+ )
+ defer { appDelegate.unregisterMainWindowContextForTesting(windowId: windowId) }
+
+ let succeeded = apply(appDelegate)
+ return (succeeded, state.isVisible, state.mode)
+ }
+
+ private func withSavedRightSidebarDockAvailability(_ body: () throws -> Void) throws {
+ let defaults = UserDefaults.standard
+ let previousDockEnabled = defaults.object(forKey: RightSidebarBetaFeatureSettings.dockEnabledKey)
+ defer {
+ if let previousDockEnabled {
+ defaults.set(previousDockEnabled, forKey: RightSidebarBetaFeatureSettings.dockEnabledKey)
+ } else {
+ defaults.removeObject(forKey: RightSidebarBetaFeatureSettings.dockEnabledKey)
+ }
+ }
+ try body()
+ }
+
func testNotificationCreateUsesExplicitSurfaceIDWhenProvided() async throws {
let socketPath = makeSocketPath("notify-surface")
let store = TerminalNotificationStore.shared
diff --git a/web/app/[locale]/docs/custom-commands/page.tsx b/web/app/[locale]/docs/custom-commands/page.tsx
index e7c6fb99c8d2..374e8dfada95 100644
--- a/web/app/[locale]/docs/custom-commands/page.tsx
+++ b/web/app/[locale]/docs/custom-commands/page.tsx
@@ -134,6 +134,7 @@ export default function CustomCommandsPage() {
"cmux.newBrowser",
"cmux.splitRight",
"cmux.splitDown",
+ "cmux.rightSidebar.toggle",
"claude"
]
}
@@ -154,6 +155,7 @@ export default function CustomCommandsPage() {
newBrowser: (chunks) => {chunks},
splitRight: (chunks) => {chunks},
splitDown: (chunks) => {chunks},
+ rightSidebarToggle: (chunks) => {chunks},
})}
@@ -468,6 +470,7 @@ export default function CustomCommandsPage() {
"cmux.newBrowser",
"cmux.splitRight",
"cmux.splitDown",
+ "cmux.rightSidebar.toggle",
{
"action": "claude",
"title": "Claude Here"
diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json
index 934556cbda0a..a68fd70d3bd1 100644
--- a/web/data/cmux.schema.json
+++ b/web/data/cmux.schema.json
@@ -20,8 +20,16 @@
},
"actions": {
"title": "actions",
- "description": "Action registry used by the surface tab bar, Command Palette, shortcuts, and plus-button menu.",
+ "description": "Action registry used by the surface tab bar, Command Palette, shortcuts, and plus-button menu. Built-in IDs include cmux.newTerminal, cmux.newBrowser, cmux.splitRight, cmux.splitDown, cmux.rightSidebar.toggle, cmux.rightSidebar.show, cmux.rightSidebar.hide, cmux.rightSidebar.focus, cmux.rightSidebar.files, cmux.rightSidebar.find, cmux.rightSidebar.vault, cmux.rightSidebar.sessions, cmux.rightSidebar.feed, and cmux.rightSidebar.dock.",
"type": "object",
+ "examples": [
+ {
+ "cmux.rightSidebar.toggle": {
+ "type": "builtin",
+ "builtin": "cmux.rightSidebar.toggle"
+ }
+ }
+ ],
"additionalProperties": true
},
"ui": {
@@ -161,7 +169,7 @@
},
"surfaceTabBarButtons": {
"title": "surfaceTabBarButtons",
- "description": "Legacy root-level surface tab bar buttons. Prefer ui.surfaceTabBar.buttons for new configs.",
+ "description": "Legacy root-level surface tab bar buttons. Prefer ui.surfaceTabBar.buttons for new configs. Button strings may reference built-in action IDs such as cmux.newTerminal, cmux.splitRight, cmux.splitDown, and cmux.rightSidebar.toggle.",
"type": "array",
"items": {
"oneOf": [
diff --git a/web/messages/en.json b/web/messages/en.json
index 8742134d9b5a..9da5687046cb 100644
--- a/web/messages/en.json
+++ b/web/messages/en.json
@@ -616,7 +616,7 @@
"schemaDesc": "A cmux.json file can contain an actions registry plus a commands array. Commands are either simple shell commands or full workspace definitions:",
"schemaIntro": "