From 4e5399691ea6a0c043c61dc7799ce3b4f651de5e Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Wed, 13 May 2026 12:53:09 -0700
Subject: [PATCH 1/9] Expose right sidebar built-in actions
---
Resources/Localizable.xcstrings | 51 ++++++
Sources/AppDelegate.swift | 41 ++++-
Sources/CmuxConfig.swift | 27 +--
Sources/CmuxSurfaceTabBarBuiltInAction.swift | 159 +++++++++++++++++-
...ntentView+RightSidebarCommandPalette.swift | 6 +-
Sources/ContentView.swift | 4 +
Sources/RightSidebarPanelView.swift | 10 ++
Sources/Workspace.swift | 12 +-
cmuxTests/CmuxConfigTests.swift | 66 ++++++++
...erminalControllerSocketSecurityTests.swift | 132 +++++++++++++++
.../[locale]/docs/custom-commands/page.tsx | 3 +
web/data/cmux.schema.json | 12 +-
web/messages/en.json | 4 +-
web/messages/ja.json | 4 +-
14 files changed, 493 insertions(+), 38 deletions(-)
diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings
index 4226edc45000..3199599c79b6 100644
--- a/Resources/Localizable.xcstrings
+++ b/Resources/Localizable.xcstrings
@@ -9880,6 +9880,57 @@
}
}
},
+ "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.openRightSidebarToolAsPane.subtitle": {
"extractionState": "manual",
"localizations": {
diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift
index 0afdc615a327..8ada90bf3f14 100644
--- a/Sources/AppDelegate.swift
+++ b/Sources/AppDelegate.swift
@@ -5694,9 +5694,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"))
}
@@ -5779,7 +5780,28 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
}
- private func rightSidebarRemoteContext(target: RightSidebarRemoteTarget) -> MainWindowContext? {
+ @discardableResult
+ func executeBuiltInRightSidebarAction(
+ _ action: CmuxSurfaceTabBarBuiltInAction,
+ preferredWindow: NSWindow? = nil,
+ onExecuted: (() -> Void)? = nil
+ ) -> Bool {
+ guard let command = action.rightSidebarRemoteCommand else {
+ return false
+ }
+ switch applyRightSidebarRemoteCommand(command, preferredWindow: preferredWindow) {
+ case .ok:
+ onExecuted?()
+ return true
+ case .state, .failure:
+ return false
+ }
+ }
+
+ private func rightSidebarRemoteContext(
+ target: RightSidebarRemoteTarget,
+ preferredWindow: NSWindow? = nil
+ ) -> MainWindowContext? {
if let windowId = target.windowId {
return mainWindowContexts.values.first(where: { $0.windowId == windowId })
}
@@ -5788,7 +5810,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
context.tabManager.tabs.contains(where: { $0.id == workspaceId })
}
}
- return preferredRegisteredMainWindowContext()
+ return preferredRegisteredMainWindowContext(preferredWindow: preferredWindow)
}
@discardableResult
@@ -13065,6 +13087,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
) -> Bool {
switch action.action {
case .builtIn(let builtIn):
+ if executeBuiltInRightSidebarAction(
+ builtIn,
+ preferredWindow: preferredWindow,
+ onExecuted: onExecuted
+ ) {
+ return true
+ }
switch builtIn {
case .newWorkspace:
context.tabManager.addWorkspace()
@@ -13117,6 +13146,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
)
if didSplit { onExecuted?() }
return didSplit
+ case .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
+ .rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
+ .rightSidebarFeed, .rightSidebarDock:
+ return false
}
case .command, .agent, .workspaceCommand:
guard let cmuxConfigStore = context.cmuxConfigStore else {
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..1584bfc32363 100644
--- a/Sources/CmuxSurfaceTabBarBuiltInAction.swift
+++ b/Sources/CmuxSurfaceTabBarBuiltInAction.swift
@@ -8,6 +8,29 @@ 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] = [
+ .rightSidebarToggle,
+ .rightSidebarShow,
+ .rightSidebarHide,
+ .rightSidebarFocus,
+ .rightSidebarFiles,
+ .rightSidebarFind,
+ .rightSidebarVault,
+ .rightSidebarSessions,
+ .rightSidebarFeed,
+ .rightSidebarDock
+ ]
init?(configID: String) {
switch configID {
@@ -25,6 +48,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 +91,126 @@ 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, .rightSidebarSessions:
+ return String(localized: "shortcut.switchRightSidebarToSessions.label", defaultValue: "Show Sidebar Vault")
+ 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 918bbffb0da7..eda8a91e2304 100644
--- a/Sources/ContentView+RightSidebarCommandPalette.swift
+++ b/Sources/ContentView+RightSidebarCommandPalette.swift
@@ -185,8 +185,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 d7962fa66af4..a0bebf1cc28d 100644
--- a/Sources/ContentView.swift
+++ b/Sources/ContentView.swift
@@ -5921,6 +5921,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 eb2b909e6b58..8a5a65292f59 100644
--- a/Sources/Workspace.swift
+++ b/Sources/Workspace.swift
@@ -14194,6 +14194,13 @@ extension Workspace: BonsplitDelegate {
?? NSApp.mainWindow
if let builtInAction = executable.builtInAction {
+ if builtInAction.rightSidebarRemoteCommand != nil {
+ _ = AppDelegate.shared?.executeBuiltInRightSidebarAction(
+ builtInAction,
+ preferredWindow: presentingWindow
+ )
+ return
+ }
switch builtInAction {
case .newWorkspace:
owningTabManager?.addWorkspace()
@@ -14203,7 +14210,10 @@ extension Workspace: BonsplitDelegate {
preferredWindow: presentingWindow,
debugSource: "surfaceTabBar.cloudVM"
)
- case .newTerminal, .newBrowser, .splitRight, .splitDown:
+ case .newTerminal, .newBrowser, .splitRight, .splitDown,
+ .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
+ .rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
+ .rightSidebarFeed, .rightSidebarDock:
break
}
return
diff --git a/cmuxTests/CmuxConfigTests.swift b/cmuxTests/CmuxConfigTests.swift
index e61550d8b6e1..683c7dc4a759 100644
--- a/cmuxTests/CmuxConfigTests.swift
+++ b/cmuxTests/CmuxConfigTests.swift
@@ -334,6 +334,72 @@ 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 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 5cc13af5265b..752e6e3969bf 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 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 9e337917d98a..9932309e8c79 100644
--- a/web/app/[locale]/docs/custom-commands/page.tsx
+++ b/web/app/[locale]/docs/custom-commands/page.tsx
@@ -133,6 +133,7 @@ export default function CustomCommandsPage() {
"cmux.newBrowser",
"cmux.splitRight",
"cmux.splitDown",
+ "cmux.rightSidebar.toggle",
"claude"
]
}
@@ -153,6 +154,7 @@ export default function CustomCommandsPage() {
newBrowser: (chunks) => {chunks},
splitRight: (chunks) => {chunks},
splitDown: (chunks) => {chunks},
+ rightSidebarToggle: (chunks) => {chunks},
})}
@@ -467,6 +469,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 74b00371abf3..933d0d80959e 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 729227e6bbf1..555acc400c05 100644
--- a/web/messages/en.json
+++ b/web/messages/en.json
@@ -435,7 +435,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": "commands still define reusable shell commands and workspace layouts. Nightly builds add an actions registry. Actions are the public IDs shared by the surface tab bar, the Command Palette, and action-level shortcuts.",
"nightlyActionRegistry": "Nightly action registry",
- "nightlyActionRegistryDesc": "actions maps stable IDs to runnable behavior. Use the built-in IDs cmux.newTerminal, cmux.newBrowser, cmux.splitRight, and cmux.splitDown to override the defaults. Use your own IDs for project-specific tools.",
+ "nightlyActionRegistryDesc": "actions maps stable IDs to runnable behavior. Use built-in IDs such as cmux.newTerminal, cmux.newBrowser, cmux.splitRight, cmux.splitDown, and cmux.rightSidebar.toggle to wire common app actions without shell commands. Right-sidebar built-ins mirror the CLI: cmux.rightSidebar.toggle, show, hide, focus, files, find, vault, sessions, feed, and dock. Use your own IDs for project-specific tools.",
"paletteDesc": "palette defaults to true. Set it to false to keep an action out of Command Palette while still making it available to the surface tab bar or a shortcut. shortcut uses the same syntax as settings shortcuts, for example cmd+shift+c or [\"cmd+k\", \"cmd+c\"].",
"iconsDesc": "ui.surfaceTabBar.buttons replaces the default button list when present. Leave out a built-in ID to hide it. Icons always use an object shape: { \"type\": \"symbol\", \"name\": \"play.circle\" }, { \"type\": \"emoji\", \"value\": \"🧪\", \"scale\": 0.9 }, or { \"type\": \"image\", \"path\": \"./icons/codex.svg\" }. Image paths are relative to the config file. Emoji scale is optional and defaults to 1. SVG, PDF, PNG, JPEG, GIF, TIFF, BMP, HEIC, HEIF, WebP, AVIF, ICO, and ICNS are supported.",
"buttonEntriesDesc": "Each button entry can be either an action ID string or a button object. Use a button object when you want the same action with a different surface label, icon, or tooltip. The resolved button title is also used as the trust prompt title.",
@@ -443,7 +443,7 @@
"customActions": "Custom actions and Command Palette",
"customActionsDesc": "An actions entry is the reusable thing cmux runs. Use actions when the same behavior should be available from the Command Palette, surface tab bar, shortcuts, or the plus-button menu. Keep commands for reusable shell commands and workspace layouts. Set palette to false when an action should stay out of Command Palette.",
"actionTypes": "Action types",
- "actionTypeBuiltin": "Alias a built-in cmux action such as cmux.newTerminal, cmux.newBrowser, cmux.splitRight, or cmux.splitDown.",
+ "actionTypeBuiltin": "Alias a built-in cmux action such as cmux.newTerminal, cmux.newBrowser, cmux.splitRight, cmux.splitDown, or cmux.rightSidebar.toggle.",
"actionTypeCommand": "Run shell text in a terminal. Use target to choose the current terminal or a new tab in the current pane.",
"actionTypeAgent": "Start a supported coding agent. Today this supports codex and claude, with optional args.",
"actionTypeWorkspaceCommand": "Run a named workspace definition from commands. Use this for multi-pane layouts, custom working directories, and startup commands.",
diff --git a/web/messages/ja.json b/web/messages/ja.json
index 103134d2ff5d..4597634cc5db 100644
--- a/web/messages/ja.json
+++ b/web/messages/ja.json
@@ -328,7 +328,7 @@
"schemaDesc": "cmux.json ファイルには actions レジストリと commands 配列を含められます。commands の各項目はシンプルなシェルコマンドまたは完全なワークスペース定義です:",
"schemaIntro": "commands は引き続き、再利用可能なシェルコマンドとワークスペースレイアウトを定義します。nightly ビルドでは actions レジストリが追加されます。アクションは、サーフェスタブバー、Command Palette、アクション単位のショートカットで共有される公開 ID です。",
"nightlyActionRegistry": "nightly アクションレジストリ",
- "nightlyActionRegistryDesc": "actions は安定した ID を実行可能な動作に対応付けます。既定値を上書きするには、組み込み ID の cmux.newTerminal、cmux.newBrowser、cmux.splitRight、cmux.splitDown を使います。プロジェクト固有のツールには独自の ID を使います。",
+ "nightlyActionRegistryDesc": "actions は安定した ID を実行可能な動作に対応付けます。cmux.newTerminal、cmux.newBrowser、cmux.splitRight、cmux.splitDown、cmux.rightSidebar.toggle などの組み込み ID を使うと、シェルコマンドなしで一般的なアプリ操作を割り当てられます。右サイドバーの組み込み ID は CLI と対応しており、cmux.rightSidebar.toggle、show、hide、focus、files、find、vault、sessions、feed、dock があります。プロジェクト固有のツールには独自の ID を使います。",
"paletteDesc": "palette は既定で true です。false にすると、サーフェスタブバーやショートカットからは使えるまま、Command Palette には表示しません。shortcut は設定ショートカットと同じ構文を使います。たとえば cmd+shift+c や [\"cmd+k\", \"cmd+c\"] です。",
"iconsDesc": "ui.surfaceTabBar.buttons がある場合、既定のボタン一覧を置き換えます。組み込み ID を省略すると非表示になります。アイコンは常にオブジェクト形式です: { \"type\": \"symbol\", \"name\": \"play.circle\" }、{ \"type\": \"emoji\", \"value\": \"🧪\", \"scale\": 0.9 }、または { \"type\": \"image\", \"path\": \"./icons/codex.svg\" }。画像パスは設定ファイルからの相対パスです。絵文字の scale は省略可能で、既定値は 1 です。SVG、PDF、PNG、JPEG、GIF、TIFF、BMP、HEIC、HEIF、WebP、AVIF、ICO、ICNS に対応しています。",
"buttonEntriesDesc": "各ボタン項目は、アクション ID 文字列またはボタンオブジェクトにできます。同じアクションに別のサーフェスラベル、アイコン、ツールチップを付けたい場合は、ボタンオブジェクトを使います。解決後のボタンタイトルは、信頼確認のタイトルにも使われます。",
@@ -336,7 +336,7 @@
"customActions": "カスタムアクションと Command Palette",
"customActionsDesc": "actions の項目は、cmux が実行する再利用可能な処理です。同じ動作を Command Palette、サーフェスタブバー、ショートカット、またはプラスボタンのメニューから使えるようにしたい場合にアクションを使います。再利用可能なシェルコマンドやワークスペースレイアウトには commands を使い続けます。Command Palette に出したくないアクションは palette を false に設定します。",
"actionTypes": "アクションの種類",
- "actionTypeBuiltin": "cmux.newTerminal、cmux.newBrowser、cmux.splitRight、cmux.splitDown などの組み込み cmux アクションに別名を付けます。",
+ "actionTypeBuiltin": "cmux.newTerminal、cmux.newBrowser、cmux.splitRight、cmux.splitDown、cmux.rightSidebar.toggle などの組み込み cmux アクションに別名を付けます。",
"actionTypeCommand": "ターミナルでシェルテキストを実行します。target で現在のターミナルまたは現在のペイン内の新しいタブを選びます。",
"actionTypeAgent": "対応しているコーディングエージェントを起動します。現在は codex と claude を、任意の引数付きでサポートしています。",
"actionTypeWorkspaceCommand": "commands から名前付きのワークスペース定義を実行します。複数ペインのレイアウト、カスタム作業ディレクトリ、起動コマンドに使います。",
From 70d104ab9e396c4bf7f485fde31b9cb115998dee Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Wed, 13 May 2026 12:58:17 -0700
Subject: [PATCH 2/9] Fix right sidebar action unit helper
---
cmuxTests/TerminalControllerSocketSecurityTests.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmuxTests/TerminalControllerSocketSecurityTests.swift b/cmuxTests/TerminalControllerSocketSecurityTests.swift
index 752e6e3969bf..c6468dca0727 100644
--- a/cmuxTests/TerminalControllerSocketSecurityTests.swift
+++ b/cmuxTests/TerminalControllerSocketSecurityTests.swift
@@ -519,7 +519,7 @@ final class TerminalControllerSocketSecurityTests: XCTestCase {
return (false, initialVisible, initialMode)
}
- return rightSidebarStateAfter(
+ return try rightSidebarStateAfter(
initialVisible: initialVisible,
initialMode: initialMode
) { appDelegate in
From 3112ee8a7e62ec2d7e28df8b6f54da0b2282d68a Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Wed, 13 May 2026 13:00:16 -0700
Subject: [PATCH 3/9] Use qualified right sidebar IDs in Japanese docs
---
web/messages/ja.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/messages/ja.json b/web/messages/ja.json
index 4597634cc5db..d91d06512b89 100644
--- a/web/messages/ja.json
+++ b/web/messages/ja.json
@@ -328,7 +328,7 @@
"schemaDesc": "cmux.json ファイルには actions レジストリと commands 配列を含められます。commands の各項目はシンプルなシェルコマンドまたは完全なワークスペース定義です:",
"schemaIntro": "commands は引き続き、再利用可能なシェルコマンドとワークスペースレイアウトを定義します。nightly ビルドでは actions レジストリが追加されます。アクションは、サーフェスタブバー、Command Palette、アクション単位のショートカットで共有される公開 ID です。",
"nightlyActionRegistry": "nightly アクションレジストリ",
- "nightlyActionRegistryDesc": "actions は安定した ID を実行可能な動作に対応付けます。cmux.newTerminal、cmux.newBrowser、cmux.splitRight、cmux.splitDown、cmux.rightSidebar.toggle などの組み込み ID を使うと、シェルコマンドなしで一般的なアプリ操作を割り当てられます。右サイドバーの組み込み ID は CLI と対応しており、cmux.rightSidebar.toggle、show、hide、focus、files、find、vault、sessions、feed、dock があります。プロジェクト固有のツールには独自の ID を使います。",
+ "nightlyActionRegistryDesc": "actions は安定した ID を実行可能な動作に対応付けます。cmux.newTerminal、cmux.newBrowser、cmux.splitRight、cmux.splitDown、cmux.rightSidebar.toggle などの組み込み ID を使うと、シェルコマンドなしで一般的なアプリ操作を割り当てられます。右サイドバーの組み込み ID は CLI と対応しており、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、cmux.rightSidebar.dock があります。プロジェクト固有のツールには独自の ID を使います。",
"paletteDesc": "palette は既定で true です。false にすると、サーフェスタブバーやショートカットからは使えるまま、Command Palette には表示しません。shortcut は設定ショートカットと同じ構文を使います。たとえば cmd+shift+c や [\"cmd+k\", \"cmd+c\"] です。",
"iconsDesc": "ui.surfaceTabBar.buttons がある場合、既定のボタン一覧を置き換えます。組み込み ID を省略すると非表示になります。アイコンは常にオブジェクト形式です: { \"type\": \"symbol\", \"name\": \"play.circle\" }、{ \"type\": \"emoji\", \"value\": \"🧪\", \"scale\": 0.9 }、または { \"type\": \"image\", \"path\": \"./icons/codex.svg\" }。画像パスは設定ファイルからの相対パスです。絵文字の scale は省略可能で、既定値は 1 です。SVG、PDF、PNG、JPEG、GIF、TIFF、BMP、HEIC、HEIF、WebP、AVIF、ICO、ICNS に対応しています。",
"buttonEntriesDesc": "各ボタン項目は、アクション ID 文字列またはボタンオブジェクトにできます。同じアクションに別のサーフェスラベル、アイコン、ツールチップを付けたい場合は、ボタンオブジェクトを使います。解決後のボタンタイトルは、信頼確認のタイトルにも使われます。",
From 9111c12aee03bc495c3165b8962ffe2c2dadc384 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Wed, 13 May 2026 13:40:56 -0700
Subject: [PATCH 4/9] Fix right sidebar action palette titles
---
Resources/Localizable.xcstrings | 17 +++++++++++++++++
Sources/CmuxSurfaceTabBarBuiltInAction.swift | 19 ++++++-------------
cmuxTests/CmuxConfigTests.swift | 18 ++++++++++++++++++
web/messages/en.json | 2 +-
4 files changed, 42 insertions(+), 14 deletions(-)
diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings
index 3199599c79b6..c239a1eec389 100644
--- a/Resources/Localizable.xcstrings
+++ b/Resources/Localizable.xcstrings
@@ -9931,6 +9931,23 @@
}
}
},
+ "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": {
diff --git a/Sources/CmuxSurfaceTabBarBuiltInAction.swift b/Sources/CmuxSurfaceTabBarBuiltInAction.swift
index 1584bfc32363..4b7365b0c4e4 100644
--- a/Sources/CmuxSurfaceTabBarBuiltInAction.swift
+++ b/Sources/CmuxSurfaceTabBarBuiltInAction.swift
@@ -19,18 +19,9 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha
case rightSidebarFeed = "cmux.rightSidebar.feed"
case rightSidebarDock = "cmux.rightSidebar.dock"
- static let rightSidebarActions: [CmuxSurfaceTabBarBuiltInAction] = [
- .rightSidebarToggle,
- .rightSidebarShow,
- .rightSidebarHide,
- .rightSidebarFocus,
- .rightSidebarFiles,
- .rightSidebarFind,
- .rightSidebarVault,
- .rightSidebarSessions,
- .rightSidebarFeed,
- .rightSidebarDock
- ]
+ static let rightSidebarActions: [CmuxSurfaceTabBarBuiltInAction] = allCases.filter {
+ $0.rightSidebarRemoteCommand != nil
+ }
init?(configID: String) {
switch configID {
@@ -134,8 +125,10 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha
return String(localized: "shortcut.switchRightSidebarToFiles.label", defaultValue: "Show Sidebar Files")
case .rightSidebarFind:
return String(localized: "shortcut.switchRightSidebarToFind.label", defaultValue: "Show Sidebar Find")
- case .rightSidebarVault, .rightSidebarSessions:
+ case .rightSidebarVault:
return String(localized: "shortcut.switchRightSidebarToSessions.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:
diff --git a/cmuxTests/CmuxConfigTests.swift b/cmuxTests/CmuxConfigTests.swift
index 683c7dc4a759..89537ee0fdf8 100644
--- a/cmuxTests/CmuxConfigTests.swift
+++ b/cmuxTests/CmuxConfigTests.swift
@@ -362,6 +362,24 @@ final class CmuxConfigDecodingTests: XCTestCase {
}
}
+ @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
diff --git a/web/messages/en.json b/web/messages/en.json
index 555acc400c05..6d8119d3ce29 100644
--- a/web/messages/en.json
+++ b/web/messages/en.json
@@ -435,7 +435,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": "commands still define reusable shell commands and workspace layouts. Nightly builds add an actions registry. Actions are the public IDs shared by the surface tab bar, the Command Palette, and action-level shortcuts.",
"nightlyActionRegistry": "Nightly action registry",
- "nightlyActionRegistryDesc": "actions maps stable IDs to runnable behavior. Use built-in IDs such as cmux.newTerminal, cmux.newBrowser, cmux.splitRight, cmux.splitDown, and cmux.rightSidebar.toggle to wire common app actions without shell commands. Right-sidebar built-ins mirror the CLI: cmux.rightSidebar.toggle, show, hide, focus, files, find, vault, sessions, feed, and dock. Use your own IDs for project-specific tools.",
+ "nightlyActionRegistryDesc": "actions maps stable IDs to runnable behavior. Use built-in IDs such as cmux.newTerminal, cmux.newBrowser, cmux.splitRight, cmux.splitDown, and cmux.rightSidebar.toggle to wire common app actions without shell commands. Right-sidebar built-ins mirror the CLI: 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. Use your own IDs for project-specific tools.",
"paletteDesc": "palette defaults to true. Set it to false to keep an action out of Command Palette while still making it available to the surface tab bar or a shortcut. shortcut uses the same syntax as settings shortcuts, for example cmd+shift+c or [\"cmd+k\", \"cmd+c\"].",
"iconsDesc": "ui.surfaceTabBar.buttons replaces the default button list when present. Leave out a built-in ID to hide it. Icons always use an object shape: { \"type\": \"symbol\", \"name\": \"play.circle\" }, { \"type\": \"emoji\", \"value\": \"🧪\", \"scale\": 0.9 }, or { \"type\": \"image\", \"path\": \"./icons/codex.svg\" }. Image paths are relative to the config file. Emoji scale is optional and defaults to 1. SVG, PDF, PNG, JPEG, GIF, TIFF, BMP, HEIC, HEIF, WebP, AVIF, ICO, and ICNS are supported.",
"buttonEntriesDesc": "Each button entry can be either an action ID string or a button object. Use a button object when you want the same action with a different surface label, icon, or tooltip. The resolved button title is also used as the trust prompt title.",
From 3c82c58390244671a38a12d79d0985b1e41a63c8 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Sun, 17 May 2026 23:15:33 -0700
Subject: [PATCH 5/9] Clarify right sidebar surface action routing
---
Sources/Workspace.swift | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift
index 8a5a65292f59..2494a704ba7f 100644
--- a/Sources/Workspace.swift
+++ b/Sources/Workspace.swift
@@ -14210,10 +14210,12 @@ extension Workspace: BonsplitDelegate {
preferredWindow: presentingWindow,
debugSource: "surfaceTabBar.cloudVM"
)
- case .newTerminal, .newBrowser, .splitRight, .splitDown,
- .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
+ case .newTerminal, .newBrowser, .splitRight, .splitDown:
+ break
+ case .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
.rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
.rightSidebarFeed, .rightSidebarDock:
+ // Right-sidebar actions return through the shared AppDelegate path above.
break
}
return
From dc4c75cdfde8f4602a4a8cb0c5a91c5d52d3de81 Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 18 May 2026 01:57:37 -0700
Subject: [PATCH 6/9] Fix right sidebar vault localization key
---
Resources/Localizable.xcstrings | 17 +++++++++++++++++
Sources/CmuxSurfaceTabBarBuiltInAction.swift | 2 +-
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings
index c239a1eec389..869104e1f190 100644
--- a/Resources/Localizable.xcstrings
+++ b/Resources/Localizable.xcstrings
@@ -105786,6 +105786,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/CmuxSurfaceTabBarBuiltInAction.swift b/Sources/CmuxSurfaceTabBarBuiltInAction.swift
index 4b7365b0c4e4..aa471310cb94 100644
--- a/Sources/CmuxSurfaceTabBarBuiltInAction.swift
+++ b/Sources/CmuxSurfaceTabBarBuiltInAction.swift
@@ -126,7 +126,7 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha
case .rightSidebarFind:
return String(localized: "shortcut.switchRightSidebarToFind.label", defaultValue: "Show Sidebar Find")
case .rightSidebarVault:
- return String(localized: "shortcut.switchRightSidebarToSessions.label", defaultValue: "Show Sidebar Vault")
+ return String(localized: "shortcut.switchRightSidebarToVault.label", defaultValue: "Show Sidebar Vault")
case .rightSidebarSessions:
return String(localized: "command.rightSidebarSessions.title", defaultValue: "Show Sidebar Sessions")
case .rightSidebarFeed:
From fe7033e6057f6c92285e96622afd875c71bf88ac Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 18 May 2026 02:09:16 -0700
Subject: [PATCH 7/9] Remove duplicated right sidebar dispatch fallbacks
---
Sources/AppDelegate.swift | 16 +++++++---------
Sources/Workspace.swift | 5 +----
2 files changed, 8 insertions(+), 13 deletions(-)
diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift
index d5e5cb178ed0..44eacf8054c2 100644
--- a/Sources/AppDelegate.swift
+++ b/Sources/AppDelegate.swift
@@ -13304,12 +13304,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
) -> Bool {
switch action.action {
case .builtIn(let builtIn):
- if executeBuiltInRightSidebarAction(
- builtIn,
- preferredWindow: preferredWindow,
- onExecuted: onExecuted
- ) {
- return true
+ if builtIn.rightSidebarRemoteCommand != nil {
+ return executeBuiltInRightSidebarAction(
+ builtIn,
+ preferredWindow: preferredWindow,
+ onExecuted: onExecuted
+ )
}
switch builtIn {
case .newWorkspace:
@@ -13363,9 +13363,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
)
if didSplit { onExecuted?() }
return didSplit
- case .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
- .rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
- .rightSidebarFeed, .rightSidebarDock:
+ default:
return false
}
case .command, .agent, .workspaceCommand:
diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift
index 3de933f35e09..9d5ce7045f45 100644
--- a/Sources/Workspace.swift
+++ b/Sources/Workspace.swift
@@ -14573,10 +14573,7 @@ extension Workspace: BonsplitDelegate {
)
case .newTerminal, .newBrowser, .splitRight, .splitDown:
break
- case .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
- .rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
- .rightSidebarFeed, .rightSidebarDock:
- // Right-sidebar actions return through the shared AppDelegate path above.
+ default:
break
}
return
From 378ed24e31a3d0b6cbca3fc97410695ba2591b7b Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 18 May 2026 19:13:25 -0700
Subject: [PATCH 8/9] Fix right sidebar built-in switch exhaustiveness
---
Sources/AppDelegate.swift | 4 +++-
Sources/Workspace.swift | 7 ++++---
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift
index 9d054fdb11d6..165b4c9e7767 100644
--- a/Sources/AppDelegate.swift
+++ b/Sources/AppDelegate.swift
@@ -13429,7 +13429,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
)
if didSplit { onExecuted?() }
return didSplit
- default:
+ case .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
+ .rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
+ .rightSidebarFeed, .rightSidebarDock:
return false
}
case .command, .agent, .workspaceCommand:
diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift
index 9db6bf48e30e..8d21120aa0a1 100644
--- a/Sources/Workspace.swift
+++ b/Sources/Workspace.swift
@@ -14530,9 +14530,10 @@ extension Workspace: BonsplitDelegate {
preferredWindow: presentingWindow,
debugSource: "surfaceTabBar.cloudVM"
)
- case .newTerminal, .newBrowser, .splitRight, .splitDown:
- break
- default:
+ case .newTerminal, .newBrowser, .splitRight, .splitDown,
+ .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
+ .rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
+ .rightSidebarFeed, .rightSidebarDock:
break
}
return
From 4e977a7f84f5a0b888a6439d40d0102105c5476d Mon Sep 17 00:00:00 2001
From: austinpower1258
Date: Mon, 18 May 2026 19:25:24 -0700
Subject: [PATCH 9/9] Centralize built-in action routing
---
Sources/AppDelegate.swift | 212 +++++++++++++++++++++++++-------------
Sources/Workspace.swift | 27 +----
2 files changed, 146 insertions(+), 93 deletions(-)
diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift
index 165b4c9e7767..ee818726f9e0 100644
--- a/Sources/AppDelegate.swift
+++ b/Sources/AppDelegate.swift
@@ -5833,15 +5833,143 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
preferredWindow: NSWindow? = nil,
onExecuted: (() -> Void)? = nil
) -> Bool {
- guard let command = action.rightSidebarRemoteCommand else {
- return false
- }
- switch applyRightSidebarRemoteCommand(command, preferredWindow: preferredWindow) {
- case .ok:
+ 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 .state, .failure:
- return false
+
+ 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
}
}
@@ -13370,70 +13498,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
) -> Bool {
switch action.action {
case .builtIn(let builtIn):
- if builtIn.rightSidebarRemoteCommand != nil {
- return executeBuiltInRightSidebarAction(
- builtIn,
- preferredWindow: preferredWindow,
- onExecuted: onExecuted
- )
- }
- 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
- case .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
- .rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
- .rightSidebarFeed, .rightSidebarDock:
- return false
- }
+ 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/Workspace.swift b/Sources/Workspace.swift
index 8d21120aa0a1..85b71e79086f 100644
--- a/Sources/Workspace.swift
+++ b/Sources/Workspace.swift
@@ -14514,28 +14514,11 @@ extension Workspace: BonsplitDelegate {
?? NSApp.mainWindow
if let builtInAction = executable.builtInAction {
- if builtInAction.rightSidebarRemoteCommand != nil {
- _ = AppDelegate.shared?.executeBuiltInRightSidebarAction(
- builtInAction,
- preferredWindow: presentingWindow
- )
- return
- }
- switch builtInAction {
- case .newWorkspace:
- owningTabManager?.addWorkspace()
- case .cloudVM:
- _ = AppDelegate.shared?.performCloudVMAction(
- tabManager: owningTabManager,
- preferredWindow: presentingWindow,
- debugSource: "surfaceTabBar.cloudVM"
- )
- case .newTerminal, .newBrowser, .splitRight, .splitDown,
- .rightSidebarToggle, .rightSidebarShow, .rightSidebarHide, .rightSidebarFocus,
- .rightSidebarFiles, .rightSidebarFind, .rightSidebarVault, .rightSidebarSessions,
- .rightSidebarFeed, .rightSidebarDock:
- break
- }
+ _ = AppDelegate.shared?.executeSurfaceTabBarBuiltInAction(
+ builtInAction,
+ tabManager: owningTabManager,
+ preferredWindow: presentingWindow
+ )
return
}