diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 0f9246cf7bbf..c4877c71addb 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -1945,6 +1945,37 @@ struct CMUXCLI { let payload = try client.sendV2(method: "workspace.rename", params: params) printV2Payload(payload, jsonOutput: jsonOutput, idFormat: idFormat, fallbackText: v2OKSummary(payload, idFormat: idFormat, kinds: ["workspace"])) + case "set-workspace-color": + let (wsArg, rem0) = parseOption(commandArgs, name: "--workspace") + let workspaceArg = wsArg ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let trailing = Array(rem0.dropFirst(rem0.first == "--" ? 1 : 0)) + guard trailing.count == 1 else { + throw CLIError(message: trailing.isEmpty + ? "set-workspace-color requires a hex color (e.g. \"#C0392B\")" + : "set-workspace-color: unexpected arguments: \(trailing.dropFirst().joined(separator: " "))") + } + let color = trailing[0].trimmingCharacters(in: .whitespacesAndNewlines) + let body = color.hasPrefix("#") ? String(color.dropFirst()) : color + guard body.count == 6, UInt64(body, radix: 16) != nil else { + throw CLIError(message: "set-workspace-color: invalid hex color \"\(color)\" (expected #RRGGBB)") + } + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + let params: [String: Any] = ["workspace_id": wsId, "color": color] + let payload = try client.sendV2(method: "workspace.set_color", params: params) + printV2Payload(payload, jsonOutput: jsonOutput, idFormat: idFormat, fallbackText: v2OKSummary(payload, idFormat: idFormat, kinds: ["workspace"])) + + case "clear-workspace-color": + let (wsArg, rem0) = parseOption(commandArgs, name: "--workspace") + let workspaceArg = wsArg ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let trailing = rem0.filter { $0 != "--" } + guard trailing.isEmpty else { + throw CLIError(message: "clear-workspace-color: unexpected arguments: \(trailing.joined(separator: " "))") + } + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + let params: [String: Any] = ["workspace_id": wsId] + let payload = try client.sendV2(method: "workspace.clear_color", params: params) + printV2Payload(payload, jsonOutput: jsonOutput, idFormat: idFormat, fallbackText: v2OKSummary(payload, idFormat: idFormat, kinds: ["workspace"])) + case "current-workspace": let response = try sendV1Command("current_workspace", client: client) if jsonOutput { @@ -6545,6 +6576,32 @@ struct CMUXCLI { cmux rename-workspace "backend logs" cmux rename-window --workspace workspace:2 "agent run" """ + case "set-workspace-color": + return """ + Usage: cmux set-workspace-color [--workspace ] + + Set the tab color for a workspace. Defaults to the current workspace. + + Flags: + --workspace Workspace to color (default: current/$CMUX_WORKSPACE_ID) + + Example: + cmux set-workspace-color "#C0392B" + cmux set-workspace-color --workspace workspace:2 "#2D1B69" + """ + case "clear-workspace-color": + return """ + Usage: cmux clear-workspace-color [--workspace ] + + Remove the custom tab color from a workspace. + + Flags: + --workspace Workspace to clear color from (default: current/$CMUX_WORKSPACE_ID) + + Example: + cmux clear-workspace-color + cmux clear-workspace-color --workspace workspace:2 + """ case "current-workspace": return """ Usage: cmux current-workspace @@ -11159,6 +11216,8 @@ struct CMUXCLI { select-workspace --workspace rename-workspace [--workspace ] rename-window [--workspace <id|ref>] <title> + set-workspace-color [--workspace <id|ref|index>] <hex> + clear-workspace-color [--workspace <id|ref|index>] current-workspace read-screen [--workspace <id|ref>] [--surface <id|ref>] [--scrollback] [--lines <n>] send [--workspace <id|ref>] [--surface <id|ref>] <text> diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 063358504754..6bc1d4d7f24b 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -2065,6 +2065,10 @@ class TerminalController { return v2Result(id: id, self.v2WorkspaceReorder(params: params)) case "workspace.rename": return v2Result(id: id, self.v2WorkspaceRename(params: params)) + case "workspace.set_color": + return v2Result(id: id, self.v2WorkspaceSetColor(params: params)) + case "workspace.clear_color": + return v2Result(id: id, self.v2WorkspaceClearColor(params: params)) case "workspace.action": return v2Result(id: id, self.v2WorkspaceAction(params: params)) case "workspace.next": @@ -2441,6 +2445,8 @@ class TerminalController { "workspace.move_to_window", "workspace.reorder", "workspace.rename", + "workspace.set_color", + "workspace.clear_color", "workspace.action", "workspace.next", "workspace.previous", @@ -3598,6 +3604,77 @@ class TerminalController { "title": title ]) } + private func v2WorkspaceSetColor(params: [String: Any]) -> V2CallResult { + guard let tabManager = v2ResolveTabManager(params: params) else { + return .err(code: "unavailable", message: "TabManager not available", data: nil) + } + guard let workspaceId = v2UUID(params, "workspace_id") else { + return .err(code: "invalid_params", message: "Missing or invalid workspace_id", data: nil) + } + guard let colorRaw = v2String(params, "color"), + !colorRaw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return .err(code: "invalid_params", message: "Missing or invalid color", data: nil) + } + + let trimmed = colorRaw.trimmingCharacters(in: .whitespacesAndNewlines) + guard let color = WorkspaceTabColorSettings.normalizedHex(trimmed) else { + return .err(code: "invalid_params", message: "Invalid hex color \"\(trimmed)\" (expected #RRGGBB)", data: nil) + } + var applied = false + v2MainSync { + guard tabManager.tabs.contains(where: { $0.id == workspaceId }) else { return } + tabManager.setTabColor(tabId: workspaceId, color: color) + applied = true + } + + guard applied else { + return .err(code: "not_found", message: "Workspace not found", data: [ + "workspace_id": workspaceId.uuidString, + "workspace_ref": v2Ref(kind: .workspace, uuid: workspaceId) + ]) + } + + let windowId = v2ResolveWindowId(tabManager: tabManager) + return .ok([ + "workspace_id": workspaceId.uuidString, + "workspace_ref": v2Ref(kind: .workspace, uuid: workspaceId), + "window_id": v2OrNull(windowId?.uuidString), + "window_ref": v2Ref(kind: .window, uuid: windowId), + "color": color + ]) + } + + private func v2WorkspaceClearColor(params: [String: Any]) -> V2CallResult { + guard let tabManager = v2ResolveTabManager(params: params) else { + return .err(code: "unavailable", message: "TabManager not available", data: nil) + } + guard let workspaceId = v2UUID(params, "workspace_id") else { + return .err(code: "invalid_params", message: "Missing or invalid workspace_id", data: nil) + } + + var cleared = false + v2MainSync { + guard tabManager.tabs.contains(where: { $0.id == workspaceId }) else { return } + tabManager.setTabColor(tabId: workspaceId, color: nil) + cleared = true + } + + guard cleared else { + return .err(code: "not_found", message: "Workspace not found", data: [ + "workspace_id": workspaceId.uuidString, + "workspace_ref": v2Ref(kind: .workspace, uuid: workspaceId) + ]) + } + + let windowId = v2ResolveWindowId(tabManager: tabManager) + return .ok([ + "workspace_id": workspaceId.uuidString, + "workspace_ref": v2Ref(kind: .workspace, uuid: workspaceId), + "window_id": v2OrNull(windowId?.uuidString), + "window_ref": v2Ref(kind: .window, uuid: windowId), + ]) + } + private func v2WorkspaceNext(params: [String: Any]) -> V2CallResult { guard let tabManager = v2ResolveTabManager(params: params) else { return .err(code: "unavailable", message: "TabManager not available", data: nil)