diff --git a/CLI/cmux.swift b/CLI/cmux.swift index c7d6b8d6..3d3daf92 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -1105,6 +1105,109 @@ struct CMUXCLI { case "claude-hook": try runClaudeHook(commandArgs: commandArgs, client: client) + case "set-status": + let (icon, r1) = parseOption(commandArgs, name: "--icon") + let (color, r2) = parseOption(r1, name: "--color") + let (wsFlag, r3) = parseOption(r2, name: "--workspace") + guard r3.count >= 2 else { + throw CLIError(message: "set-status requires and ") + } + let key = r3[0] + let value = r3.dropFirst().joined(separator: " ") + guard !value.isEmpty else { + throw CLIError(message: "set-status requires a non-empty value") + } + let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + var socketCmd = "set_status \(key) \(socketQuote(value))" + if let icon { socketCmd += " --icon=\(socketQuote(icon))" } + if let color { socketCmd += " --color=\(socketQuote(color))" } + socketCmd += " --tab=\(wsId)" + let response = try sendV1Command(socketCmd, client: client) + print(response) + + case "clear-status": + let (wsFlag, csRemaining) = parseOption(commandArgs, name: "--workspace") + guard let key = csRemaining.first else { + throw CLIError(message: "clear-status requires a ") + } + let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + let response = try sendV1Command("clear_status \(key) --tab=\(wsId)", client: client) + print(response) + + case "list-status": + let (wsFlag, _) = parseOption(commandArgs, name: "--workspace") + let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + let response = try sendV1Command("list_status --tab=\(wsId)", client: client) + print(response) + + case "set-progress": + let (label, spR1) = parseOption(commandArgs, name: "--label") + let (wsFlag, spR2) = parseOption(spR1, name: "--workspace") + guard let valueStr = spR2.first else { + throw CLIError(message: "set-progress requires a progress value (0.0-1.0)") + } + let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + var socketCmd = "set_progress \(valueStr)" + if let label { socketCmd += " --label=\(socketQuote(label))" } + socketCmd += " --tab=\(wsId)" + let response = try sendV1Command(socketCmd, client: client) + print(response) + + case "clear-progress": + let (wsFlag, _) = parseOption(commandArgs, name: "--workspace") + let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + let response = try sendV1Command("clear_progress --tab=\(wsId)", client: client) + print(response) + + case "log": + let (level, r1) = parseOption(commandArgs, name: "--level") + let (source, r2) = parseOption(r1, name: "--source") + let (wsFlag, r3) = parseOption(r2, name: "--workspace") + // Strip leading "--" separator if present + let positional = r3.first == "--" ? Array(r3.dropFirst()) : r3 + let message = positional.joined(separator: " ") + guard !message.isEmpty else { + throw CLIError(message: "log requires a message") + } + let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + var socketCmd = "log" + if let level { socketCmd += " --level=\(level)" } + if let source { socketCmd += " --source=\(socketQuote(source))" } + socketCmd += " --tab=\(wsId) -- \(socketQuote(message))" + let response = try sendV1Command(socketCmd, client: client) + print(response) + + case "clear-log": + let (wsFlag, _) = parseOption(commandArgs, name: "--workspace") + let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + let response = try sendV1Command("clear_log --tab=\(wsId)", client: client) + print(response) + + case "list-log": + let (limitStr, r1) = parseOption(commandArgs, name: "--limit") + let (wsFlag, _) = parseOption(r1, name: "--workspace") + let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + var socketCmd = "list_log" + if let limitStr { socketCmd += " --limit=\(limitStr)" } + socketCmd += " --tab=\(wsId)" + let response = try sendV1Command(socketCmd, client: client) + print(response) + + case "sidebar-state": + let (wsFlag, _) = parseOption(commandArgs, name: "--workspace") + let workspaceArg = wsFlag ?? (windowId == nil ? ProcessInfo.processInfo.environment["CMUX_WORKSPACE_ID"] : nil) + let wsId = try resolveWorkspaceId(workspaceArg, client: client) + let response = try sendV1Command("sidebar_state --tab=\(wsId)", client: client) + print(response) + case "set-app-focus": guard let value = commandArgs.first else { throw CLIError(message: "set-app-focus requires a value") } let response = try sendV1Command("set_app_focus \(value)", client: client) @@ -3453,6 +3556,130 @@ struct CMUXCLI { cmux notify --title "Build done" --body "All tests passed" cmux notify --title "Error" --subtitle "test.swift" --body "Line 42: syntax error" """ + case "set-status": + return """ + Usage: cmux set-status [flags] + + Set a sidebar status entry for a workspace. Status entries appear as + pills in the sidebar tab row. Use a unique key so different tools + (e.g. "claude_code", "build") can manage their own entries. + + Flags: + --icon Icon name (e.g. "sparkle", "hammer") + --color <#hex> Pill color (e.g. "#ff9500") + --workspace Target workspace (default: $CMUX_WORKSPACE_ID) + + Example: + cmux set-status build "compiling" --icon hammer --color "#ff9500" + cmux set-status deploy "v1.2.3" --workspace workspace:2 + """ + case "clear-status": + return """ + Usage: cmux clear-status [flags] + + Remove a sidebar status entry by key. + + Flags: + --workspace Target workspace (default: $CMUX_WORKSPACE_ID) + + Example: + cmux clear-status build + """ + case "list-status": + return """ + Usage: cmux list-status [flags] + + List all sidebar status entries for a workspace. + + Flags: + --workspace Target workspace (default: $CMUX_WORKSPACE_ID) + + Example: + cmux list-status + cmux list-status --workspace workspace:2 + """ + case "set-progress": + return """ + Usage: cmux set-progress <0.0-1.0> [flags] + + Set a progress bar in the sidebar for a workspace. + + Flags: + --label Label shown next to the progress bar + --workspace Target workspace (default: $CMUX_WORKSPACE_ID) + + Example: + cmux set-progress 0.5 --label "Building..." + cmux set-progress 1.0 --label "Done" + """ + case "clear-progress": + return """ + Usage: cmux clear-progress [flags] + + Clear the sidebar progress bar for a workspace. + + Flags: + --workspace Target workspace (default: $CMUX_WORKSPACE_ID) + + Example: + cmux clear-progress + """ + case "log": + return """ + Usage: cmux log [flags] [--] + + Append a log entry to the sidebar for a workspace. + + Flags: + --level Log level: info, progress, success, warning, error (default: info) + --source Source label (e.g. "build", "test") + --workspace Target workspace (default: $CMUX_WORKSPACE_ID) + + Example: + cmux log "Build started" + cmux log --level error --source build "Compilation failed" + cmux log --level success -- "All 42 tests passed" + """ + case "clear-log": + return """ + Usage: cmux clear-log [flags] + + Clear all sidebar log entries for a workspace. + + Flags: + --workspace Target workspace (default: $CMUX_WORKSPACE_ID) + + Example: + cmux clear-log + """ + case "list-log": + return """ + Usage: cmux list-log [flags] + + List sidebar log entries for a workspace. + + Flags: + --limit Show only the last N entries + --workspace Target workspace (default: $CMUX_WORKSPACE_ID) + + Example: + cmux list-log + cmux list-log --limit 5 + """ + case "sidebar-state": + return """ + Usage: cmux sidebar-state [flags] + + Dump all sidebar metadata for a workspace (cwd, git branch, ports, + status entries, progress, log entries). + + Flags: + --workspace Target workspace (default: $CMUX_WORKSPACE_ID) + + Example: + cmux sidebar-state + cmux sidebar-state --workspace workspace:2 + """ case "claude-hook": return """ Usage: cmux claude-hook [flags] @@ -3515,6 +3742,20 @@ struct CMUXCLI { return true } + /// Escape and quote a string for safe embedding in a v1 socket command. + /// The socket tokenizer treats `\` and `"` as special inside quoted strings, + /// so both must be escaped before wrapping in double quotes. Newlines and + /// carriage returns must also be escaped since the socket protocol uses + /// newline as the message terminator. + private func socketQuote(_ s: String) -> String { + let escaped = s + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + return "\"\(escaped)\"" + } + private func parseOption(_ args: [String], name: String) -> (String?, [String]) { var remaining: [String] = [] var value: String? @@ -4721,6 +4962,18 @@ struct CMUXCLI { list-notifications clear-notifications claude-hook [--workspace ] [--surface ] + + # sidebar metadata commands + set-status [--icon ] [--color <#hex>] [--workspace ] + clear-status [--workspace ] + list-status [--workspace ] + set-progress <0.0-1.0> [--label ] [--workspace ] + clear-progress [--workspace ] + log [--level ] [--source ] [--workspace ] [--] + clear-log [--workspace ] + list-log [--limit ] [--workspace ] + sidebar-state [--workspace ] + set-app-focus simulate-app-active diff --git a/web/app/docs/api/page.tsx b/web/app/docs/api/page.tsx index 6f7322e7..bd24302b 100644 --- a/web/app/docs/api/page.tsx +++ b/web/app/docs/api/page.tsx @@ -5,7 +5,7 @@ import { Callout } from "../../components/callout"; export const metadata: Metadata = { title: "API Reference", description: - "cmux CLI and Unix socket API reference. Workspace management, split panes, input control, notifications, environment variables, and detection methods.", + "cmux CLI and Unix socket API reference. Workspace management, split panes, input control, notifications, sidebar metadata (status, progress, logs), environment variables, and detection methods.", }; function Cmd({ @@ -281,6 +281,74 @@ cmux list-notifications --json`} socket={`{"id":"notif-clear","method":"notification.clear","params":{}}`} /> +

Sidebar metadata commands

+

+ Set status pills, progress bars, and log entries in the sidebar for any + workspace. Useful for build scripts, CI integrations, and AI coding + agents that want to surface state at a glance. +

+ + `} + /> + `} + /> + `} + /> + `} + /> + `} + /> + -- Compilation failed`} + /> + `} + /> + `} + /> + `} + /> +

Utility commands