Add sidebar metadata CLI subcommands and API docs (#305)
* Add sidebar metadata CLI subcommands and API docs Expose set-status, clear-status, list-status, set-progress, clear-progress, log, clear-log, list-log, and sidebar-state as proper CLI subcommands with --help support and usage() listing. Previously these only existed as raw socket commands. Also adds a "Sidebar metadata commands" section to the docs site API reference page. * Quote multi-word values in socket command strings Fix set-progress --label and log message forwarding to properly quote values before sending to the socket tokenizer. Without quoting, multi-word labels like "Build step one" would be split into separate tokens. Also quote --source values for consistency. * Fix socket quoting: escape backslashes and quote status values Add socketQuote() helper that escapes both backslashes and double quotes before wrapping in quotes. Apply it to: - set-status value (prevents --flags in values being parsed as options) - set-status --icon and --color values - set-progress --label - log --source and message text Fixes values like "pytest --maxfail=1" or "C:\new\build" being mangled by the socket tokenizer. * Escape newlines in socketQuote to prevent socket framing breakage The socket protocol uses newline as message terminator, so embedded newlines/carriage returns in values would truncate the command. * Parse flags before positionals in set-status, clear-status, set-progress Fixes flags-first invocation like `cmux set-status --workspace workspace:2 build compiling` which previously grabbed `--workspace` as the key. Now all flags are extracted first, then positional args are validated.
This commit is contained in:
parent
6d64ca938e
commit
f7457055f1
2 changed files with 322 additions and 1 deletions
253
CLI/cmux.swift
253
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 <key> and <value>")
|
||||
}
|
||||
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 <key>")
|
||||
}
|
||||
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 <key> <value> [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 <name> Icon name (e.g. "sparkle", "hammer")
|
||||
--color <#hex> Pill color (e.g. "#ff9500")
|
||||
--workspace <id|ref> 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 <key> [flags]
|
||||
|
||||
Remove a sidebar status entry by key.
|
||||
|
||||
Flags:
|
||||
--workspace <id|ref> 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 <id|ref> 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 <text> Label shown next to the progress bar
|
||||
--workspace <id|ref> 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 <id|ref> Target workspace (default: $CMUX_WORKSPACE_ID)
|
||||
|
||||
Example:
|
||||
cmux clear-progress
|
||||
"""
|
||||
case "log":
|
||||
return """
|
||||
Usage: cmux log [flags] [--] <message>
|
||||
|
||||
Append a log entry to the sidebar for a workspace.
|
||||
|
||||
Flags:
|
||||
--level <level> Log level: info, progress, success, warning, error (default: info)
|
||||
--source <name> Source label (e.g. "build", "test")
|
||||
--workspace <id|ref> 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 <id|ref> 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 <n> Show only the last N entries
|
||||
--workspace <id|ref> 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 <id|ref> Target workspace (default: $CMUX_WORKSPACE_ID)
|
||||
|
||||
Example:
|
||||
cmux sidebar-state
|
||||
cmux sidebar-state --workspace workspace:2
|
||||
"""
|
||||
case "claude-hook":
|
||||
return """
|
||||
Usage: cmux claude-hook <session-start|stop|notification> [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 <session-start|stop|notification> [--workspace <id|ref>] [--surface <id|ref>]
|
||||
|
||||
# sidebar metadata commands
|
||||
set-status <key> <value> [--icon <name>] [--color <#hex>] [--workspace <id|ref>]
|
||||
clear-status <key> [--workspace <id|ref>]
|
||||
list-status [--workspace <id|ref>]
|
||||
set-progress <0.0-1.0> [--label <text>] [--workspace <id|ref>]
|
||||
clear-progress [--workspace <id|ref>]
|
||||
log [--level <level>] [--source <name>] [--workspace <id|ref>] [--] <message>
|
||||
clear-log [--workspace <id|ref>]
|
||||
list-log [--limit <n>] [--workspace <id|ref>]
|
||||
sidebar-state [--workspace <id|ref>]
|
||||
|
||||
set-app-focus <active|inactive|clear>
|
||||
simulate-app-active
|
||||
|
||||
|
|
|
|||
|
|
@ -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":{}}`}
|
||||
/>
|
||||
|
||||
<h2>Sidebar metadata commands</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<Cmd
|
||||
name="set-status"
|
||||
desc="Set a sidebar status pill. Use a unique key so different tools can manage their own entries."
|
||||
cli={`cmux set-status build "compiling" --icon hammer --color "#ff9500"
|
||||
cmux set-status deploy "v1.2.3" --workspace workspace:2`}
|
||||
socket={`set_status build compiling --icon=hammer --color=#ff9500 --tab=<workspace-uuid>`}
|
||||
/>
|
||||
<Cmd
|
||||
name="clear-status"
|
||||
desc="Remove a sidebar status entry by key."
|
||||
cli={`cmux clear-status build`}
|
||||
socket={`clear_status build --tab=<workspace-uuid>`}
|
||||
/>
|
||||
<Cmd
|
||||
name="list-status"
|
||||
desc="List all sidebar status entries for a workspace."
|
||||
cli={`cmux list-status`}
|
||||
socket={`list_status --tab=<workspace-uuid>`}
|
||||
/>
|
||||
<Cmd
|
||||
name="set-progress"
|
||||
desc="Set a progress bar in the sidebar (0.0 to 1.0)."
|
||||
cli={`cmux set-progress 0.5 --label "Building..."
|
||||
cmux set-progress 1.0 --label "Done"`}
|
||||
socket={`set_progress 0.5 --label=Building... --tab=<workspace-uuid>`}
|
||||
/>
|
||||
<Cmd
|
||||
name="clear-progress"
|
||||
desc="Clear the sidebar progress bar."
|
||||
cli={`cmux clear-progress`}
|
||||
socket={`clear_progress --tab=<workspace-uuid>`}
|
||||
/>
|
||||
<Cmd
|
||||
name="log"
|
||||
desc="Append a log entry to the sidebar. Levels: info, progress, success, warning, error."
|
||||
cli={`cmux log "Build started"
|
||||
cmux log --level error --source build "Compilation failed"
|
||||
cmux log --level success -- "All 42 tests passed"`}
|
||||
socket={`log --level=error --source=build --tab=<workspace-uuid> -- Compilation failed`}
|
||||
/>
|
||||
<Cmd
|
||||
name="clear-log"
|
||||
desc="Clear all sidebar log entries."
|
||||
cli={`cmux clear-log`}
|
||||
socket={`clear_log --tab=<workspace-uuid>`}
|
||||
/>
|
||||
<Cmd
|
||||
name="list-log"
|
||||
desc="List sidebar log entries."
|
||||
cli={`cmux list-log
|
||||
cmux list-log --limit 5`}
|
||||
socket={`list_log --limit=5 --tab=<workspace-uuid>`}
|
||||
/>
|
||||
<Cmd
|
||||
name="sidebar-state"
|
||||
desc="Dump all sidebar metadata (cwd, git branch, ports, status, progress, logs)."
|
||||
cli={`cmux sidebar-state
|
||||
cmux sidebar-state --workspace workspace:2`}
|
||||
socket={`sidebar_state --tab=<workspace-uuid>`}
|
||||
/>
|
||||
|
||||
<h2>Utility commands</h2>
|
||||
|
||||
<Cmd
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue