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
242 changes: 242 additions & 0 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3406,6 +3406,13 @@ struct CMUXCLI {
)
print(response)

case "right-sidebar":
try forwardRightSidebarCommand(
commandArgs: commandArgs,
client: client,
windowOverride: windowId
)

case "claude-hook":
cliTelemetry.breadcrumb("claude-hook.dispatch")
do {
Expand Down Expand Up @@ -10028,6 +10035,34 @@ struct CMUXCLI {
cmux sidebar-state
cmux sidebar-state --workspace workspace:2
"""
case "right-sidebar":
return String(localized: "cli.rightSidebar.usage", defaultValue: """
Usage: cmux right-sidebar <command> [flags]

Control the right sidebar from the CLI.

Commands:
toggle Toggle right sidebar visibility
show Show the right sidebar
hide Hide the right sidebar
focus Focus the current right sidebar mode
set <files|find|vault|sessions|feed|dock>
Show, switch mode, and focus
mode Print {"visible":bool,"mode":string}
files|find|vault|sessions|feed|dock
Alias for show + set + focus

Flags:
--workspace <id|ref|index> Target the window containing a workspace
--window <id|ref|index> Target a window
--no-focus With set, switch mode without moving focus

Examples:
cmux right-sidebar toggle
cmux right-sidebar set find
cmux right-sidebar set vault --no-focus
cmux right-sidebar mode
""")
case "set-app-focus":
return """
Usage: cmux set-app-focus <active|inactive|clear>
Expand Down Expand Up @@ -10342,6 +10377,212 @@ struct CMUXCLI {
return try sendV1Command(command, client: client)
}

private struct RightSidebarCLIArguments {
let positional: [String]
let workspace: String?
let window: String?
let noFocus: Bool
}

private func forwardRightSidebarCommand(
commandArgs: [String],
client: SocketClient,
windowOverride: String?
) throws {
let parsed = try parseRightSidebarCLIArguments(commandArgs)
let socketArgs = try rightSidebarSocketArguments(from: parsed)
let windowId = try resolveRightSidebarWindowId(parsed.window ?? windowOverride, client: client)
let workspaceId = try resolveRightSidebarWorkspaceId(parsed.workspace, windowId: windowId, client: client)

var forwardedArgs = socketArgs
if let workspaceId {
forwardedArgs.append("--tab=\(workspaceId)")
}
if let windowId {
forwardedArgs.append("--window=\(windowId)")
}

let command = (["right_sidebar"] + forwardedArgs)
.map(shellQuote)
.joined(separator: " ")
let response = try sendV1Command(command, client: client)
if parsed.positional.first?.lowercased() == "mode" {
print(response)
}
Comment thread
lawrencecchen marked this conversation as resolved.
}

private func parseRightSidebarCLIArguments(_ args: [String]) throws -> RightSidebarCLIArguments {
var positional: [String] = []
var workspace: String?
var window: String?
var noFocus = false
var index = 0

while index < args.count {
let arg = args[index]
switch arg {
case "--workspace":
guard index + 1 < args.count else {
throw CLIError(message: String(localized: "cli.rightSidebar.error.workspaceRequiresValue", defaultValue: "right-sidebar: --workspace requires an id"))
}
workspace = args[index + 1]
index += 2
case "--window":
guard index + 1 < args.count else {
throw CLIError(message: String(localized: "cli.rightSidebar.error.windowRequiresValue", defaultValue: "right-sidebar: --window requires an id"))
}
window = args[index + 1]
index += 2
case "--no-focus":
noFocus = true
index += 1
default:
if arg.hasPrefix("--workspace=") {
workspace = String(arg.dropFirst("--workspace=".count))
index += 1
} else if arg.hasPrefix("--window=") {
window = String(arg.dropFirst("--window=".count))
index += 1
} else if arg.hasPrefix("--") {
throw CLIError(message: String(localized: "cli.rightSidebar.error.unknownFlag", defaultValue: "right-sidebar: unknown flag '\(arg)'"))
} else {
positional.append(arg)
index += 1
}
}
}

return RightSidebarCLIArguments(
positional: positional,
workspace: workspace,
window: window,
noFocus: noFocus
)
}

private func rightSidebarSocketArguments(from parsed: RightSidebarCLIArguments) throws -> [String] {
guard let action = parsed.positional.first?.lowercased() else {
throw CLIError(message: String(localized: "cli.rightSidebar.error.missingCommand", defaultValue: "right-sidebar requires a subcommand"))
}

switch action {
case "toggle", "show", "hide", "focus", "mode":
guard parsed.positional.count == 1 else {
throw CLIError(message: String(localized: "cli.rightSidebar.error.unexpectedArguments", defaultValue: "right-sidebar \(action) received unexpected arguments"))
}
guard !parsed.noFocus else {
throw CLIError(message: String(localized: "cli.rightSidebar.error.noFocusOnlySet", defaultValue: "right-sidebar: --no-focus is only valid with set"))
}
return [action]

case "set":
guard parsed.positional.count == 2 else {
throw CLIError(message: String(localized: "cli.rightSidebar.error.setRequiresMode", defaultValue: "right-sidebar set requires a mode: files, find, vault, sessions, feed, or dock"))
}
let mode = parsed.positional[1].trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard isRightSidebarCLIMode(mode) else {
throw CLIError(message: String(localized: "cli.rightSidebar.error.unknownMode", defaultValue: "Unknown right-sidebar mode '\(parsed.positional[1])'"))
}
var args = ["set", mode]
if parsed.noFocus {
args.append("--no-focus")
}
return args
Comment thread
cursor[bot] marked this conversation as resolved.

case "files", "find", "vault", "sessions", "feed", "dock":
guard parsed.positional.count == 1 else {
throw CLIError(message: String(localized: "cli.rightSidebar.error.unexpectedArguments", defaultValue: "right-sidebar \(action) received unexpected arguments"))
}
guard !parsed.noFocus else {
throw CLIError(message: String(localized: "cli.rightSidebar.error.noFocusOnlySet", defaultValue: "right-sidebar: --no-focus is only valid with set"))
}
return ["set", action]

default:
throw CLIError(message: String(localized: "cli.rightSidebar.error.unknownCommand", defaultValue: "Unknown right-sidebar command '\(action)'"))
}
}

private func isRightSidebarCLIMode(_ value: String) -> Bool {
switch value {
case "files", "find", "vault", "sessions", "feed", "dock":
return true
default:
return false
}
}

private func resolveRightSidebarWindowId(_ raw: String?, client: SocketClient) throws -> String? {
guard let normalized = try normalizeWindowHandle(raw, client: client) else { return nil }
return try resolvedRightSidebarHandleID(
normalized,
expectedRefKind: "window",
invalidMessage: String(localized: "cli.rightSidebar.error.invalidWindow", defaultValue: "Invalid window handle: \(normalized)"),
missingRefMessage: String(localized: "cli.rightSidebar.error.windowRefNotFound", defaultValue: "Window ref not found"),
listMethod: "window.list",
listKey: "windows",
client: client
)
}

private func resolveRightSidebarWorkspaceId(
_ raw: String?,
windowId: String?,
client: SocketClient
) throws -> String? {
var params: [String: Any] = [:]
if let windowId {
params["window_id"] = windowId
}
guard let normalized = try normalizeWorkspaceHandle(raw, client: client, windowHandle: windowId) else { return nil }
return try resolvedRightSidebarHandleID(
normalized,
expectedRefKind: "workspace",
invalidMessage: String(localized: "cli.rightSidebar.error.invalidWorkspace", defaultValue: "Invalid workspace handle: \(normalized)"),
missingRefMessage: String(localized: "cli.rightSidebar.error.workspaceRefNotFound", defaultValue: "Workspace ref not found"),
listMethod: "workspace.list",
listKey: "workspaces",
listParams: params,
client: client
)
}

private func resolvedRightSidebarHandleID(
_ handle: String,
expectedRefKind: String,
invalidMessage: String,
missingRefMessage: String,
listMethod: String,
listKey: String,
listParams: [String: Any] = [:],
client: SocketClient
) throws -> String {
let trimmed = handle.trimmingCharacters(in: .whitespacesAndNewlines)
if isUUID(trimmed) { return trimmed }
let refIndex: Int?
if isHandleRef(trimmed) {
let pieces = trimmed.split(separator: ":", omittingEmptySubsequences: false)
guard pieces.count == 2, pieces[0].lowercased() == expectedRefKind else {
throw CLIError(message: invalidMessage)
}
refIndex = Int(pieces[1])
} else {
refIndex = nil
}

let listed = try client.sendV2(method: listMethod, params: listParams)
let items = listed[listKey] as? [[String: Any]] ?? []
for item in items {
guard let id = item["id"] as? String else { continue }
if id == trimmed ||
(item["ref"] as? String) == trimmed ||
(refIndex != nil && intFromAny(item["index"]) == refIndex) {
return id
}
}
throw CLIError(message: missingRefMessage)
}
Comment thread
cursor[bot] marked this conversation as resolved.

/// Pick the display handle for an item dict based on --id-format.
private func textHandle(_ item: [String: Any], idFormat: CLIIDFormat) -> String {
let ref = item["ref"] as? String
Expand Down Expand Up @@ -20638,6 +20879,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
notify --title <text> [--subtitle <text>] [--body <text>] [--workspace <id|ref>] [--surface <id|ref>]
list-notifications
clear-notifications
right-sidebar <toggle|show|hide|focus|set|mode|files|find|vault|sessions|feed|dock> [--workspace <id|ref|index>] [--window <id|ref|index>] [--no-focus]
set-status <key> <value> [--workspace <id|ref>] [--icon <name>] [--color <#hex>]
clear-status <key> [--workspace <id|ref>]
list-status [--workspace <id|ref>]
Expand Down
4 changes: 4 additions & 0 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@
B35750000000000000000006 /* VaultAgentProcessScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35750000000000000000005 /* VaultAgentProcessScanner.swift */; };
FE003102 /* SessionIndexView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003002 /* SessionIndexView.swift */; };
B37A00000000000000000003 /* RightSidebarMode+Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37A00000000000000000004 /* RightSidebarMode+Availability.swift */; };
B37A0000000000000000000D /* RightSidebarRemoteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37A0000000000000000000E /* RightSidebarRemoteCommand.swift */; };
FE003103 /* RightSidebarPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003003 /* RightSidebarPanelView.swift */; };
FE003104 /* SessionIndexStore+CodexSQL.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003004 /* SessionIndexStore+CodexSQL.swift */; };
FE003105 /* RovoDevIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003005 /* RovoDevIndex.swift */; };
Expand Down Expand Up @@ -573,6 +574,7 @@
B7F9A603B7F9A603B7F9A603 /* WindowChromeMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowChromeMetrics.swift; sourceTree = "<group>"; };
B7F9A605B7F9A605B7F9A605 /* RightSidebarChromeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightSidebarChromeStyle.swift; sourceTree = "<group>"; };
B37A00000000000000000004 /* RightSidebarMode+Availability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RightSidebarMode+Availability.swift"; sourceTree = "<group>"; };
B37A0000000000000000000E /* RightSidebarRemoteCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightSidebarRemoteCommand.swift; sourceTree = "<group>"; };
B37A00000000000000000006 /* MainWindowFocusTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindowFocusTypes.swift; sourceTree = "<group>"; };
A11EAB000000000000000001 /* AppearanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettings.swift; sourceTree = "<group>"; };
A11EAC000000000000000001 /* GhosttyTerminalAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyTerminalAppearance.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1311,6 +1313,7 @@
B7F9A605B7F9A605B7F9A605 /* RightSidebarChromeStyle.swift */,
B7F9A603B7F9A603B7F9A603 /* WindowChromeMetrics.swift */,
B37A00000000000000000004 /* RightSidebarMode+Availability.swift */,
B37A0000000000000000000E /* RightSidebarRemoteCommand.swift */,
FE003003 /* RightSidebarPanelView.swift */,
D0C0D0C0D0C0D0C0D0C0D002 /* DockPanelView.swift */,
D0C0D0C0D0C0D0C0D0C0D004 /* DockEmptyView.swift */,
Expand Down Expand Up @@ -1949,6 +1952,7 @@
B7F9A604B7F9A604B7F9A604 /* RightSidebarChromeStyle.swift in Sources */,
B7F9A602B7F9A602B7F9A602 /* WindowChromeMetrics.swift in Sources */,
B37A00000000000000000003 /* RightSidebarMode+Availability.swift in Sources */,
B37A0000000000000000000D /* RightSidebarRemoteCommand.swift in Sources */,
FE003103 /* RightSidebarPanelView.swift in Sources */,
D0C0D0C0D0C0D0C0D0C0D001 /* DockPanelView.swift in Sources */,
D0C0D0C0D0C0D0C0D0C0D003 /* DockEmptyView.swift in Sources */,
Expand Down
Loading
Loading