Repository navigation
Add set-workspace-color and clear-workspace-color CLI commands #1825
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ca5bbad
563d9d8
2bdb009
ae365a8
db7aa46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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)") | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1958
to
+1960
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Require explicit
Suggested fix- let body = color.hasPrefix("#") ? String(color.dropFirst()) : color
- guard body.count == 6, UInt64(body, radix: 16) != nil else {
+ guard color.hasPrefix("#") else {
+ throw CLIError(message: "set-workspace-color: invalid hex color \"\(color)\" (expected `#RRGGBB`)")
+ }
+ let body = String(color.dropFirst())
+ guard body.count == 6, UInt64(body, radix: 16) != nil else {
throw CLIError(message: "set-workspace-color: invalid hex color \"\(color)\" (expected `#RRGGBB`)")
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentional — CLI is lenient (accepts both #RRGGBB and bare RRGGBB) for ergonomics, matching normalizedHex which strips the # prefix internally. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Optional follow-ups for clarity only:
Want me to open a small follow-up to tweak the help text/message, or leave as-is? ✏️ Learnings added
🧠 Learnings used |
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| 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"])) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||
| 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 <id|ref|index>] <hex> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Set the tab color for a workspace. Defaults to the current workspace. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Flags: | ||||||||||||||||||||||||||||||||||||||
| --workspace <id|ref|index> 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 <id|ref|index>] | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Remove the custom tab color from a workspace. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Flags: | ||||||||||||||||||||||||||||||||||||||
| --workspace <id|ref|index> 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 <id|ref> | ||||||||||||||||||||||||||||||||||||||
| rename-workspace [--workspace <id|ref>] <title> | ||||||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| } | ||
|
Comment on lines
+3620
to
+3622
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the invalid-color error message to include alpha hex format. The message currently says only 💡 Proposed fix- guard let color = WorkspaceTabColorSettings.normalizedHex(trimmed) else {
- return .err(code: "invalid_params", message: "Invalid hex color \"\(trimmed)\" (expected `#RRGGBB`)", data: nil)
+ guard let color = WorkspaceTabColorSettings.normalizedHex(trimmed) else {
+ return .err(
+ code: "invalid_params",
+ message: "Invalid hex color \"\(trimmed)\" (expected `#RRGGBB` or `#RRGGBBAA`)",
+ data: nil
+ )
}🤖 Prompt for AI Agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not applicable — normalizedHex only accepts 6-digit hex (body.count == 6 in TabManager.swift:367). The error message correctly says #RRGGBB only. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ Learnings added
🧠 Learnings used |
||
| 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 | ||
| ]) | ||
|
Comment on lines
+3614
to
+3644
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Additionally, even for a valid hex input, the response echoes back the raw (non-normalized) string (e.g. The fix is to validate and normalize via
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| 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) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
set-workspace-colorregressed by rejecting previously accepted 8-digit hex colors (#RRGGBBAA).Prompt for AI agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a regression — normalizedHex (TabManager.swift:367) only accepts 6-digit hex (body.count == 6). The previous CLI code accepting 8-digit would pass CLI validation but fail server-side. Aligning CLI to 6-digit only is correct.