diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index abe5cbe6275d..ab072561a39c 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -1,7 +1,7 @@ # cmux-owned Swift file length budget. # Format: max_linesrelative path # Reduce counts as files shrink. CI fails if tracked files exceed this budget. -34202 CLI/cmux.swift +34274 CLI/cmux.swift 17778 Sources/AppDelegate.swift 16674 Sources/ContentView.swift 14785 Sources/TerminalController.swift @@ -9,7 +9,7 @@ 12361 Sources/Workspace.swift 12093 Sources/GhosttyTerminalView.swift 12046 cmuxTests/AppDelegateShortcutRoutingTests.swift -9331 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift +9763 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift 7925 Sources/Panels/BrowserPanelView.swift 7355 cmuxTests/WorkspaceUnitTests.swift 7221 cmuxTests/WorkspaceRemoteConnectionTests.swift @@ -186,7 +186,7 @@ 531 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift 531 Sources/App/WorkspaceRuntimeSettings.swift 530 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttyRuntime.swift -528 cmuxTests/CLINotifyProcessTestSupport.swift +530 cmuxTests/CLINotifyProcessTestSupport.swift 528 cmuxUITests/AutomationSocketUITests.swift 527 CLI/CLISocketPathResolver.swift 524 CLI/CMUXCLI+AutoNaming.swift diff --git a/CLI/CMUXCLI+ListSurfaces.swift b/CLI/CMUXCLI+ListSurfaces.swift new file mode 100644 index 000000000000..64535094fd37 --- /dev/null +++ b/CLI/CMUXCLI+ListSurfaces.swift @@ -0,0 +1,349 @@ +import Foundation + +extension CMUXCLI { + private struct ListSurfacesCommandOptions { + let includeAllWindows: Bool + let workspaceHandle: String? + let windowHandle: String? + let jsonOutput: Bool + let terminalOnly: Bool + } + + func runListSurfacesCommand( + commandName: String, + commandArgs: [String], + client: SocketClient, + jsonOutput: Bool, + idFormat: CLIIDFormat, + terminalOnly: Bool, + windowOverride: String? = nil + ) throws { + let options = try parseListSurfacesCommandOptions( + commandArgs, + commandName: commandName, + terminalOnly: terminalOnly, + windowOverride: windowOverride + ) + let payload = try buildListSurfacesPayload( + options: options, + client: client + ) + if jsonOutput || options.jsonOutput { + print(jsonString(formatIDs(payload, mode: idFormat))) + } else { + let surfaces = payload["surfaces"] as? [[String: Any]] ?? [] + print(renderListSurfacesText( + surfaces: surfaces, + idFormat: idFormat, + terminalOnly: terminalOnly + )) + } + } + + func validateListSurfacesCommandSyntax( + commandName: String, + commandArgs: [String], + terminalOnly: Bool, + windowOverride: String? = nil + ) throws { + _ = try parseListSurfacesCommandOptions( + commandArgs, + commandName: commandName, + terminalOnly: terminalOnly, + windowOverride: windowOverride + ) + } + + private func parseListSurfacesCommandOptions( + _ args: [String], + commandName: String, + terminalOnly: Bool, + windowOverride: String? + ) throws -> ListSurfacesCommandOptions { + let workspaceWasProvided = listSurfaceOptionWasProvided(args, name: "--workspace") + let (workspaceOpt, rem0) = parseOption(args, name: "--workspace") + if listSurfaceOptionRequiresHandle(workspaceOpt, wasProvided: workspaceWasProvided) { + throw CLIError(message: String(format: String( + localized: "cli.listSurfaces.error.missingHandleOption", + defaultValue: "%@ requires %@ " + ), commandName, "--workspace")) + } + let windowWasProvided = listSurfaceOptionWasProvided(rem0, name: "--window") + let (windowOpt, rem1) = parseOption(rem0, name: "--window") + if listSurfaceOptionRequiresHandle(windowOpt, wasProvided: windowWasProvided) { + throw CLIError(message: String(format: String( + localized: "cli.listSurfaces.error.missingHandleOption", + defaultValue: "%@ requires %@ " + ), commandName, "--window")) + } + + var includeAll = false + var jsonOutput = false + var remaining: [String] = [] + for arg in rem1 { + if arg == "--all" { + includeAll = true + continue + } + if arg == "--json" { + jsonOutput = true + continue + } + remaining.append(arg) + } + + if let unknown = remaining.first(where: { $0.hasPrefix("--") }) { + throw CLIError(message: String(format: String( + localized: "cli.listSurfaces.error.unknownFlag", + defaultValue: "%@: unknown flag '%@'. Known flags: --all --workspace --window --json" + ), commandName, unknown)) + } + if let extra = remaining.first { + throw CLIError(message: String(format: String( + localized: "cli.listSurfaces.error.unexpectedArgument", + defaultValue: "%@: unexpected argument '%@'" + ), commandName, extra)) + } + let effectiveWindowOpt = windowOpt ?? windowOverride + if includeAll, let scopedOption = effectiveWindowOpt != nil ? "--window" : (workspaceOpt != nil ? "--workspace" : nil) { + throw CLIError(message: String(format: String( + localized: "cli.listSurfaces.error.windowWithAll", + defaultValue: "%@: %@ cannot be combined with --all" + ), commandName, scopedOption)) + } + + let includeAllByDefault = effectiveWindowOpt == nil && workspaceOpt == nil + return ListSurfacesCommandOptions( + includeAllWindows: includeAll || includeAllByDefault, + workspaceHandle: workspaceOpt, + windowHandle: effectiveWindowOpt, + jsonOutput: jsonOutput, + terminalOnly: terminalOnly + ) + } + + private func listSurfaceOptionWasProvided(_ args: [String], name: String) -> Bool { + var pastTerminator = false + for arg in args { + if arg == "--" { + pastTerminator = true + continue + } + if !pastTerminator, arg == name || arg.hasPrefix("\(name)=") { + return true + } + } + return false + } + + private func listSurfaceOptionRequiresHandle(_ value: String?, wasProvided: Bool) -> Bool { + guard wasProvided else { return false } + guard let value else { return true } + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty || trimmed.hasPrefix("-") + } + + private func buildListSurfacesPayload( + options: ListSurfacesCommandOptions, + client: SocketClient + ) throws -> [String: Any] { + let treePayload = try buildTreePayload( + options: TreeCommandOptions( + includeAllWindows: options.includeAllWindows, + workspaceHandle: options.workspaceHandle, + windowHandle: options.windowHandle, + jsonOutput: options.jsonOutput + ), + client: client + ) + let surfaces = flattenTreeSurfaces( + payload: treePayload, + terminalOnly: options.terminalOnly + ) + return [ + "count": surfaces.count, + "terminal_only": options.terminalOnly, + "surfaces": surfaces + ] + } + + private func flattenTreeSurfaces( + payload: [String: Any], + terminalOnly: Bool + ) -> [[String: Any]] { + let windows = payload["windows"] as? [[String: Any]] ?? [] + let activeSurfaceHandle = listSurfaceActiveHandle(payload) + var result: [[String: Any]] = [] + + for window in windows { + let workspaces = window["workspaces"] as? [[String: Any]] ?? [] + for workspace in workspaces { + let panes = workspace["panes"] as? [[String: Any]] ?? [] + for pane in panes { + let surfaces = pane["surfaces"] as? [[String: Any]] ?? [] + for surface in surfaces { + let type = ((surface["type"] as? String) ?? "") + .trimmingCharacters(in: .whitespacesAndNewlines) + .lowercased() + if terminalOnly, type != "terminal" { + continue + } + + var row = surface + copyTreeListContext(into: &row, kind: "surface", from: surface) + copyTreeListContext(into: &row, kind: "pane", from: pane) + copyTreeListContext(into: &row, kind: "workspace", from: workspace) + copyTreeListContext(into: &row, kind: "window", from: window) + copyTreeListValue(into: &row, key: "surface_index", from: surface["index"] ?? surface["index_in_pane"]) + copyTreeListValue(into: &row, key: "pane_index", from: pane["index"]) + copyTreeListValue(into: &row, key: "workspace_index", from: workspace["index"]) + copyTreeListValue(into: &row, key: "workspace_title", from: workspace["title"]) + copyTreeListValue(into: &row, key: "window_index", from: window["index"]) + copyTreeListValue(into: &row, key: "window_key", from: window["key"]) + copyTreeListValue(into: &row, key: "pane_focused", from: pane["focused"]) + row["active"] = listSurfaceMatchesHandle(surface, handle: activeSurfaceHandle) + || listSurfaceBool(surface["active"]) + result.append(row) + } + } + } + } + + return result + } + + private func listSurfaceActiveHandle(_ payload: [String: Any]) -> String? { + guard let active = payload["active"] as? [String: Any] else { return nil } + return listSurfaceHandle(active, refKey: "surface_ref", idKey: "surface_id") + } + + private func listSurfaceMatchesHandle(_ surface: [String: Any], handle: String?) -> Bool { + guard let handle else { return false } + return ["ref", "id", "surface_ref", "surface_id"].contains { key in + listSurfaceHandleString(surface[key]) == handle + } + } + + private func listSurfaceHandle( + _ item: [String: Any], + refKey: String, + idKey: String + ) -> String? { + if let ref = listSurfaceHandleString(item[refKey]) { + return ref + } + return listSurfaceHandleString(item[idKey]) + } + + private func listSurfaceHandleString(_ value: Any?) -> String? { + guard let string = listSurfaceDebugString(value)?.trimmingCharacters(in: .whitespacesAndNewlines), + !string.isEmpty else { return nil } + return string + } + + private func listSurfaceBool(_ value: Any?) -> Bool { + guard let value, !(value is NSNull) else { return false } + if let bool = value as? Bool { + return bool + } + if let number = value as? NSNumber { + return number.boolValue + } + if let string = value as? String { + switch string.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { + case "1", "true", "yes": + return true + default: + return false + } + } + return false + } + + private func copyTreeListContext( + into row: inout [String: Any], + kind: String, + from source: [String: Any] + ) { + copyTreeListValue(into: &row, key: "\(kind)_id", from: source["id"]) + copyTreeListValue(into: &row, key: "\(kind)_ref", from: source["ref"]) + } + + private func copyTreeListValue( + into row: inout [String: Any], + key: String, + from value: Any? + ) { + guard row[key] == nil, let value else { return } + row[key] = value + } + + private func renderListSurfacesText( + surfaces: [[String: Any]], + idFormat: CLIIDFormat, + terminalOnly: Bool + ) -> String { + guard !surfaces.isEmpty else { + return terminalOnly + ? String(localized: "cli.listTerminals.empty", defaultValue: "No terminal surfaces") + : String(localized: "cli.listSurfaces.empty", defaultValue: "No surfaces") + } + + return surfaces.map { row in + var parts: [String] = [] + let prefix = (row["active"] as? Bool) == true ? "* " : " " + let surface = formatHandle(row, kind: "surface", idFormat: idFormat) + ?? listSurfaceFallbackHandle(row, idFormat: idFormat) + let rawType = ((row["type"] as? String) ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let type = rawType.isEmpty ? "unknown" : rawType + parts.append("\(prefix)\(surface)") + parts.append(type) + parts.append("window=\(formatHandle(row, kind: "window", idFormat: idFormat) ?? "?")") + parts.append("workspace=\(formatHandle(row, kind: "workspace", idFormat: idFormat) ?? "?")") + parts.append("pane=\(formatHandle(row, kind: "pane", idFormat: idFormat) ?? "?")") + if let title = listSurfaceDebugString(row["title"]), !title.isEmpty { + parts.append("\"\(title.replacingOccurrences(of: "\"", with: "\\\""))\"") + } + if let tty = listSurfaceDebugString(row["tty"]), !tty.isEmpty { + parts.append("tty=\(tty)") + } + if type.lowercased() == "browser", + let url = listSurfaceDebugString(row["url"]), + !url.isEmpty { + parts.append(url) + } + return parts.joined(separator: " ") + } + .joined(separator: "\n") + } + + private func listSurfaceFallbackHandle( + _ item: [String: Any], + idFormat: CLIIDFormat + ) -> String { + let ref = item["ref"] as? String + let id = item["id"] as? String + switch idFormat { + case .refs: + return ref ?? id ?? "?" + case .uuids: + return id ?? ref ?? "?" + case .both: + if let ref, let id { + return "\(ref) \(id)" + } + return ref ?? id ?? "?" + } + } + + private func listSurfaceDebugString(_ value: Any?) -> String? { + guard let value, !(value is NSNull) else { return nil } + if let string = value as? String { + return string + } + if let number = value as? NSNumber { + return number.stringValue + } + return String(describing: value) + } +} diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 40e79bac8280..ecf20f77b46a 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -3354,6 +3354,9 @@ struct CMUXCLI { if normalizedCommand == "surface", commandArgs.first?.lowercased() == "resume" { return false } + if normalizedCommand == "list-surfaces" || normalizedCommand == "list-terminals" { + return false + } return true } @@ -3496,6 +3499,15 @@ struct CMUXCLI { return } + if command == "list-surfaces" || command == "list-terminals" { + try validateListSurfacesCommandSyntax( + commandName: command, + commandArgs: commandArgs, + terminalOnly: command == "list-terminals", + windowOverride: windowId + ) + } + // If the argument is a path (not a known command), open a workspace there. if shouldOpenAsPathArgument(command), explicitSocketPath == nil { try openPath(command) @@ -4429,6 +4441,28 @@ struct CMUXCLI { } } + case "list-surfaces": + try runListSurfacesCommand( + commandName: command, + commandArgs: commandArgs, + client: client, + jsonOutput: jsonOutput, + idFormat: idFormat, + terminalOnly: false, + windowOverride: windowId + ) + + case "list-terminals": + try runListSurfacesCommand( + commandName: command, + commandArgs: commandArgs, + client: client, + jsonOutput: jsonOutput, + idFormat: idFormat, + terminalOnly: true, + windowOverride: windowId + ) + case "tree": try runTreeCommand(commandArgs: commandArgs, client: client, jsonOutput: jsonOutput, idFormat: idFormat) @@ -5549,6 +5583,8 @@ struct CMUXCLI { "list-panels", "list-panes", "list-status", + "list-surfaces", + "list-terminals", "list-windows", "list-workspaces", "log", @@ -15069,6 +15105,40 @@ struct CMUXCLI { cmux list-pane-surfaces cmux list-pane-surfaces --workspace workspace:2 --pane pane:1 """ + case "list-surfaces": + return String(localized: "cli.help.listSurfaces", defaultValue: """ + Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json] + + List cmux surfaces across windows, workspaces, and panes. + By default, lists every window so this works from an SSH shell outside cmux. + + Flags: + --workspace Restrict to one workspace + --window Restrict to one window + --all List all windows (default when no window/workspace is specified) + --json Structured JSON output + + Example: + cmux list-surfaces + cmux list-surfaces --workspace workspace:2 + """) + case "list-terminals": + return String(localized: "cli.help.listTerminals", defaultValue: """ + Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json] + + List terminal surfaces across windows, workspaces, and panes. + By default, lists every window so this works from an SSH shell outside cmux. + + Flags: + --workspace Restrict to one workspace + --window Restrict to one window + --all List all windows (default when no window/workspace is specified) + --json Structured JSON output + + Example: + cmux list-terminals + cmux list-terminals --workspace workspace:2 + """) case "tree": return """ Usage: cmux tree [flags] @@ -16739,7 +16809,7 @@ struct CMUXCLI { return parts.joined(separator: " ") } - private struct TreeCommandOptions { + struct TreeCommandOptions { let includeAllWindows: Bool let workspaceHandle: String? let windowHandle: String? @@ -16984,7 +17054,7 @@ struct CMUXCLI { } } - private func buildTreePayload( + func buildTreePayload( options: TreeCommandOptions, client: SocketClient ) throws -> [String: Any] { @@ -34014,6 +34084,8 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) { new-split [--workspace ] [--surface ] [--panel ] [--window ] [--focus ] list-panes [--workspace ] [--window ] list-pane-surfaces [--workspace ] [--pane ] [--window ] + list-surfaces [--workspace ] [--window ] [--all] [--json] + list-terminals [--workspace ] [--window ] [--all] [--json] tree [--all] [--workspace ] [--window ] top [--all] [--workspace ] [--window ] [--processes] [--sort ] [--flat] [--format ] memory [--all] [--workspace ] [--groups ] diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index cae4cd198368..d631699fc542 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -160681,6 +160681,14 @@ } } }, + "cli.help.listSurfaces": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"bs":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"da":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"de":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"en":{"stringUnit":{"state":"translated","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"es":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"fr":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"it":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"ja":{"stringUnit":{"state":"translated","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nウィンドウ、ワークスペース、ペインを横断して cmux サーフェスを一覧表示します。\n既定ではすべてのウィンドウを一覧表示するため、cmux 外の SSH シェルから使えます。\n\nFlags:\n --workspace 1 つのワークスペースに限定\n --window 1 つのウィンドウに限定\n --all すべてのウィンドウを一覧表示(window/workspace 未指定時の既定)\n --json 構造化 JSON 出力\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"km":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"ko":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"nb":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"pl":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"ru":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"th":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"tr":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"uk":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-surfaces [--workspace ] [--window ] [--all] [--json]\n\nList cmux surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-surfaces\n cmux list-surfaces --workspace workspace:2"}}}}, + "cli.help.listTerminals": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"bs":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"da":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"de":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"en":{"stringUnit":{"state":"translated","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"es":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"fr":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"it":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"ja":{"stringUnit":{"state":"translated","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nウィンドウ、ワークスペース、ペインを横断してターミナルサーフェスを一覧表示します。\n既定ではすべてのウィンドウを一覧表示するため、cmux 外の SSH シェルから使えます。\n\nFlags:\n --workspace 1 つのワークスペースに限定\n --window 1 つのウィンドウに限定\n --all すべてのウィンドウを一覧表示(window/workspace 未指定時の既定)\n --json 構造化 JSON 出力\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"km":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"ko":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"nb":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"pl":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"ru":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"th":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"tr":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"uk":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Usage: cmux list-terminals [--workspace ] [--window ] [--all] [--json]\n\nList terminal surfaces across windows, workspaces, and panes.\nBy default, lists every window so this works from an SSH shell outside cmux.\n\nFlags:\n --workspace Restrict to one workspace\n --window Restrict to one window\n --all List all windows (default when no window/workspace is specified)\n --json Structured JSON output\n\nExample:\n cmux list-terminals\n cmux list-terminals --workspace workspace:2"}}}}, + "cli.listSurfaces.empty": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"bs":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"da":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"de":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"en":{"stringUnit":{"state":"translated","value":"No surfaces"}},"es":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"fr":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"it":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"ja":{"stringUnit":{"state":"translated","value":"サーフェスはありません"}},"km":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"ko":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"nb":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"pl":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"ru":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"th":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"tr":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"uk":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"No surfaces"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"No surfaces"}}}}, + "cli.listTerminals.empty": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"bs":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"da":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"de":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"en":{"stringUnit":{"state":"translated","value":"No terminal surfaces"}},"es":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"fr":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"it":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"ja":{"stringUnit":{"state":"translated","value":"ターミナルサーフェスはありません"}},"km":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"ko":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"nb":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"pl":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"ru":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"th":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"tr":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"uk":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"No terminal surfaces"}}}}, + "cli.listSurfaces.error.missingHandleOption": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"bs":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"da":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"de":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"en":{"stringUnit":{"state":"translated","value":"%@ requires %@ "}},"es":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"fr":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"it":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"ja":{"stringUnit":{"state":"translated","value":"%@ には %@ が必要です"}},"km":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"ko":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"nb":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"pl":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"ru":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"th":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"tr":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"uk":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"%@ requires %@ "}}}}, + "cli.listSurfaces.error.unknownFlag": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"bs":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"da":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"de":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"en":{"stringUnit":{"state":"translated","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"es":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"fr":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"it":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"ja":{"stringUnit":{"state":"translated","value":"%@: 不明なフラグ '%@' です。既知のフラグ: --all --workspace --window --json"}},"km":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"ko":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"nb":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"pl":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"ru":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"th":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"tr":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"uk":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"%@: unknown flag '%@'. Known flags: --all --workspace --window --json"}}}}, + "cli.listSurfaces.error.unexpectedArgument": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"bs":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"da":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"de":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"en":{"stringUnit":{"state":"translated","value":"%@: unexpected argument '%@'"}},"es":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"fr":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"it":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"ja":{"stringUnit":{"state":"translated","value":"%@: 予期しない引数 '%@'"}},"km":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"ko":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"nb":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"pl":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"ru":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"th":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"tr":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"uk":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"%@: unexpected argument '%@'"}}}}, + "cli.listSurfaces.error.windowWithAll": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"bs":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"da":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"de":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"en":{"stringUnit":{"state":"translated","value":"%@: %@ cannot be combined with --all"}},"es":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"fr":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"it":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"ja":{"stringUnit":{"state":"translated","value":"%@: %@ は --all と同時に指定できません"}},"km":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"ko":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"nb":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"pl":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"ru":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"th":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"tr":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"uk":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"%@: %@ cannot be combined with --all"}}}}, "cli.top.error.processDiagnosticsUnsupported": { "extractionState": "manual", "localizations": { diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index e6bcb1aa93c1..3558393d4e0a 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -188,6 +188,7 @@ B9000046A1B2C3D4E5F60719 /* CMUXCLI+ExecutableResolution.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000047A1B2C3D4E5F60719 /* CMUXCLI+ExecutableResolution.swift */; }; B9000061A1B2C3D4E5F60719 /* CMUXCLI+HermesAgentHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000060A1B2C3D4E5F60719 /* CMUXCLI+HermesAgentHooks.swift */; }; B9000052A1B2C3D4E5F60719 /* CMUXCLI+InstallPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000053A1B2C3D4E5F60719 /* CMUXCLI+InstallPreview.swift */; }; + B9000075A1B2C3D4E5F60719 /* CMUXCLI+ListSurfaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000074A1B2C3D4E5F60719 /* CMUXCLI+ListSurfaces.swift */; }; B9000071A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000070A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift */; }; D7AB0000000000000000000D /* CMUXCLI+MoveTabToNewWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB0000000000000000000E /* CMUXCLI+MoveTabToNewWorkspace.swift */; }; B9000072A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000073A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift */; }; @@ -1128,6 +1129,7 @@ B9000047A1B2C3D4E5F60719 /* CMUXCLI+ExecutableResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+ExecutableResolution.swift"; sourceTree = ""; }; B9000060A1B2C3D4E5F60719 /* CMUXCLI+HermesAgentHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+HermesAgentHooks.swift"; sourceTree = ""; }; B9000053A1B2C3D4E5F60719 /* CMUXCLI+InstallPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+InstallPreview.swift"; sourceTree = ""; }; + B9000074A1B2C3D4E5F60719 /* CMUXCLI+ListSurfaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+ListSurfaces.swift"; sourceTree = ""; }; B9000070A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+Memory.swift"; sourceTree = ""; }; D7AB0000000000000000000E /* CMUXCLI+MoveTabToNewWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+MoveTabToNewWorkspace.swift"; sourceTree = ""; }; B9000073A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+OmpExtension.swift"; sourceTree = ""; }; @@ -2526,6 +2528,7 @@ B9000069A1B2C3D4E5F60719 /* CMUXCLI+AmpExtension.swift */, C0D3F1F00000000000000102 /* CMUXCLI+CodexFireAndForgetHooks.swift */, B9000073A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift */, + B9000074A1B2C3D4E5F60719 /* CMUXCLI+ListSurfaces.swift */, B9000070A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift */, B9000031A1B2C3D4E5F60719 /* CMUXCLI+DocsSettings.swift */, B900002DA1B2C3D4E5F60719 /* CMUXCLI+ThemeSupport.swift */, @@ -3738,6 +3741,7 @@ B9000046A1B2C3D4E5F60719 /* CMUXCLI+ExecutableResolution.swift in Sources */, B9000061A1B2C3D4E5F60719 /* CMUXCLI+HermesAgentHooks.swift in Sources */, B9000052A1B2C3D4E5F60719 /* CMUXCLI+InstallPreview.swift in Sources */, + B9000075A1B2C3D4E5F60719 /* CMUXCLI+ListSurfaces.swift in Sources */, B9000071A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift in Sources */, D7AB0000000000000000000D /* CMUXCLI+MoveTabToNewWorkspace.swift in Sources */, B9000072A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift in Sources */, diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift index 74fb0cb18e29..92322aeb99cb 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift @@ -4681,6 +4681,438 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { ]) } + func testListTerminalsFlattensAllWindowsAndFiltersNonTerminals() throws { + let cliPath = try bundledCLIPath() + let socketPath = makeSocketPath("listterm") + let listenerFD = try bindUnixSocket(at: socketPath) + let state = MockSocketServerState() + + defer { + Darwin.close(listenerFD) + unlink(socketPath) + } + + let serverHandled = startMockServer(listenerFD: listenerFD, state: state) { line in + guard let payload = self.jsonObject(line), + let id = payload["id"] as? String, + let method = payload["method"] as? String else { + return self.malformedRequestResponse(raw: line) + } + XCTAssertEqual(method, "system.tree") + let params = payload["params"] as? [String: Any] ?? [:] + XCTAssertEqual(params["all_windows"] as? Bool, true) + XCTAssertNil(params["workspace_id"]) + XCTAssertNil(params["window_id"]) + return self.v2Response( + id: id, + ok: true, + result: [ + "active": [ + "window_ref": "window:1", + "workspace_ref": "workspace:2", + "pane_ref": "pane:3", + "surface_ref": "surface:4", + ], + "caller": NSNull(), + "windows": [ + [ + "id": "11111111-1111-1111-1111-111111111111", + "ref": "window:1", + "index": 0, + "workspaces": [ + [ + "id": "22222222-2222-2222-2222-222222222222", + "ref": "workspace:2", + "index": 1, + "title": "Ops", + "panes": [ + [ + "id": "33333333-3333-3333-3333-333333333333", + "ref": "pane:3", + "index": 0, + "focused": true, + "surfaces": [ + [ + "id": "44444444-4444-4444-4444-444444444444", + "ref": "surface:4", + "index": 0, + "type": "terminal", + "selected": true, + "focused": true, + "title": "agent shell", + "tty": "/dev/ttys123", + ], + [ + "id": "55555555-5555-5555-5555-555555555555", + "ref": "surface:5", + "index": 1, + "type": "browser", + "title": "Docs", + "url": "https://example.test", + ], + [ + "id": "66666666-6666-6666-6666-666666666666", + "ref": "surface:6", + "index": 2, + "type": "terminal", + "title": "other shell", + "tty": "/dev/ttys456", + ], + ], + ], + ], + ], + ], + ], + [ + "id": "77777777-7777-7777-7777-777777777777", + "ref": "window:7", + "index": 1, + "workspaces": [ + [ + "id": "88888888-8888-8888-8888-888888888888", + "ref": "workspace:8", + "index": 0, + "title": "Background", + "panes": [ + [ + "id": "99999999-9999-9999-9999-999999999999", + "ref": "pane:9", + "index": 0, + "focused": true, + "surfaces": [ + [ + "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "ref": "surface:10", + "index": 0, + "type": "terminal", + "selected": true, + "focused": true, + "title": "background shell", + "tty": "/dev/ttys789", + ], + ], + ], + ], + ], + ], + ], + ], + ] + ) + } + + var environment = ProcessInfo.processInfo.environment + for key in Array(environment.keys) where key.hasPrefix("CMUX_") { + environment.removeValue(forKey: key) + } + environment["CMUX_SOCKET_PATH"] = socketPath + environment["CMUX_CLI_SENTRY_DISABLED"] = "1" + + let result = runProcess( + executablePath: cliPath, + arguments: ["list-terminals"], + environment: environment, + timeout: 5 + ) + + wait(for: [serverHandled], timeout: 5) + XCTAssertFalse(result.timedOut, result.stderr) + XCTAssertEqual(result.status, 0, result.stderr) + XCTAssertTrue(result.stderr.isEmpty, result.stderr) + XCTAssertTrue(result.stdout.contains("* surface:4"), result.stdout) + XCTAssertFalse(result.stdout.contains("* surface:6"), result.stdout) + XCTAssertFalse(result.stdout.contains("* surface:10"), result.stdout) + let starredRows = result.stdout.split(separator: "\n").filter { $0.hasPrefix("* ") } + XCTAssertEqual(starredRows.count, 1, result.stdout) + XCTAssertTrue(result.stdout.contains("surface:4"), result.stdout) + XCTAssertTrue(result.stdout.contains("surface:6"), result.stdout) + XCTAssertTrue(result.stdout.contains("surface:10"), result.stdout) + XCTAssertTrue(result.stdout.contains("terminal"), result.stdout) + XCTAssertTrue(result.stdout.contains("window=window:1"), result.stdout) + XCTAssertTrue(result.stdout.contains("workspace=workspace:2"), result.stdout) + XCTAssertTrue(result.stdout.contains("pane=pane:3"), result.stdout) + XCTAssertTrue(result.stdout.contains(#""agent shell""#), result.stdout) + XCTAssertTrue(result.stdout.contains("tty=/dev/ttys123"), result.stdout) + XCTAssertTrue(result.stdout.contains(#""other shell""#), result.stdout) + XCTAssertTrue(result.stdout.contains("tty=/dev/ttys456"), result.stdout) + XCTAssertTrue(result.stdout.contains(#""background shell""#), result.stdout) + XCTAssertTrue(result.stdout.contains("tty=/dev/ttys789"), result.stdout) + XCTAssertFalse(result.stdout.contains("surface:5"), result.stdout) + XCTAssertFalse(result.stdout.contains("Docs"), result.stdout) + XCTAssertEqual( + state.snapshot().compactMap { self.jsonObject($0)?["method"] as? String }, + ["system.tree"] + ) + } + + func testListSurfacesUsesWorkspaceScopeAndIncludesBrowsers() throws { + let cliPath = try bundledCLIPath() + let socketPath = makeSocketPath("listsurf") + let listenerFD = try bindUnixSocket(at: socketPath) + let state = MockSocketServerState() + + defer { + Darwin.close(listenerFD) + unlink(socketPath) + } + + let serverHandled = startMockServer(listenerFD: listenerFD, state: state) { line in + guard let payload = self.jsonObject(line), + let id = payload["id"] as? String, + let method = payload["method"] as? String else { + return self.malformedRequestResponse(raw: line) + } + XCTAssertEqual(method, "system.tree") + let params = payload["params"] as? [String: Any] ?? [:] + XCTAssertEqual(params["all_windows"] as? Bool, false) + XCTAssertEqual(params["workspace_id"] as? String, "workspace:2") + XCTAssertNil(params["window_id"]) + return self.v2Response( + id: id, + ok: true, + result: [ + "windows": [ + [ + "id": "11111111-1111-1111-1111-111111111111", + "ref": "window:1", + "index": 0, + "workspaces": [ + [ + "id": "22222222-2222-2222-2222-222222222222", + "ref": "workspace:2", + "index": 1, + "title": "Docs", + "panes": [ + [ + "id": "33333333-3333-3333-3333-333333333333", + "ref": "pane:3", + "index": 0, + "surfaces": [ + [ + "id": "55555555-5555-5555-5555-555555555555", + "ref": "surface:5", + "index": 1, + "type": "browser", + "title": "Docs", + "url": "https://example.test", + ], + ], + ], + ], + ], + ], + ], + ], + ] + ) + } + + var environment = ProcessInfo.processInfo.environment + for key in Array(environment.keys) where key.hasPrefix("CMUX_") { + environment.removeValue(forKey: key) + } + environment["CMUX_SOCKET_PATH"] = socketPath + environment["CMUX_CLI_SENTRY_DISABLED"] = "1" + + let result = runProcess( + executablePath: cliPath, + arguments: ["list-surfaces", "--workspace", "workspace:2"], + environment: environment, + timeout: 5 + ) + + wait(for: [serverHandled], timeout: 5) + XCTAssertFalse(result.timedOut, result.stderr) + XCTAssertEqual(result.status, 0, result.stderr) + XCTAssertTrue(result.stderr.isEmpty, result.stderr) + XCTAssertTrue(result.stdout.contains("surface:5"), result.stdout) + XCTAssertTrue(result.stdout.contains("browser"), result.stdout) + XCTAssertTrue(result.stdout.contains("window=window:1"), result.stdout) + XCTAssertTrue(result.stdout.contains("workspace=workspace:2"), result.stdout) + XCTAssertTrue(result.stdout.contains("pane=pane:3"), result.stdout) + XCTAssertTrue(result.stdout.contains("https://example.test"), result.stdout) + } + + func testListSurfacesHonorsGlobalWindowOverride() throws { + let cliPath = try bundledCLIPath() + let socketPath = makeSocketPath("listsurf-window") + let listenerFD = try bindUnixSocket(at: socketPath) + let state = MockSocketServerState() + let windowId = "11111111-1111-1111-1111-111111111111" + + defer { + Darwin.close(listenerFD) + unlink(socketPath) + } + + let serverHandled = startMockServer(listenerFD: listenerFD, state: state) { line in + guard let payload = self.jsonObject(line), + let id = payload["id"] as? String, + let method = payload["method"] as? String else { + return self.malformedRequestResponse(raw: line) + } + XCTAssertEqual(method, "system.tree") + let params = payload["params"] as? [String: Any] ?? [:] + XCTAssertEqual(params["all_windows"] as? Bool, false) + XCTAssertEqual(params["window_id"] as? String, windowId) + XCTAssertNil(params["workspace_id"]) + return self.v2Response( + id: id, + ok: true, + result: [ + "active": NSNull(), + "caller": NSNull(), + "windows": [], + ] + ) + } + + var environment = ProcessInfo.processInfo.environment + for key in Array(environment.keys) where key.hasPrefix("CMUX_") { + environment.removeValue(forKey: key) + } + environment["CMUX_SOCKET_PATH"] = socketPath + environment["CMUX_CLI_SENTRY_DISABLED"] = "1" + + let result = runProcess( + executablePath: cliPath, + arguments: ["--window", windowId, "list-surfaces", "--json"], + environment: environment, + timeout: 5 + ) + + wait(for: [serverHandled], timeout: 5) + XCTAssertFalse(result.timedOut, result.stderr) + XCTAssertEqual(result.status, 0, result.stderr) + XCTAssertTrue(result.stderr.isEmpty, result.stderr) + let payload = try jsonPayload(from: result.stdout) + XCTAssertEqual(payload["count"] as? Int, 0) + } + + func testListSurfacesCommandWinsOverSameNamedPath() throws { + let cliPath = try bundledCLIPath() + let socketPath = makeSocketPath("listsurf-path") + let listenerFD = try bindUnixSocket(at: socketPath) + let state = MockSocketServerState() + let root = try makeTemporaryDirectory(prefix: "cmux-list-surfaces-path") + try FileManager.default.createDirectory( + at: root.appendingPathComponent("list-surfaces", isDirectory: true), + withIntermediateDirectories: true + ) + + defer { + Darwin.close(listenerFD) + unlink(socketPath) + } + + let serverHandled = startMockServer(listenerFD: listenerFD, state: state) { line in + guard let payload = self.jsonObject(line), + let id = payload["id"] as? String, + let method = payload["method"] as? String else { + return self.malformedRequestResponse(raw: line) + } + XCTAssertEqual(method, "system.tree") + let params = payload["params"] as? [String: Any] ?? [:] + XCTAssertEqual(params["all_windows"] as? Bool, true) + XCTAssertNil(params["window_id"]) + XCTAssertNil(params["workspace_id"]) + return self.v2Response( + id: id, + ok: true, + result: [ + "active": NSNull(), + "caller": NSNull(), + "windows": [], + ] + ) + } + + var environment = ProcessInfo.processInfo.environment + for key in Array(environment.keys) where key.hasPrefix("CMUX_") { + environment.removeValue(forKey: key) + } + environment["CMUX_SOCKET_PATH"] = socketPath + environment["CMUX_CLI_SENTRY_DISABLED"] = "1" + + let result = runProcess( + executablePath: cliPath, + arguments: ["list-surfaces", "--json"], + environment: environment, + currentDirectoryURL: root, + timeout: 5 + ) + + wait(for: [serverHandled], timeout: 5) + XCTAssertFalse(result.timedOut, result.stderr) + XCTAssertEqual(result.status, 0, result.stderr) + XCTAssertTrue(result.stderr.isEmpty, result.stderr) + let payload = try jsonPayload(from: result.stdout) + XCTAssertEqual(payload["count"] as? Int, 0) + } + + func testListSurfacesRejectsWorkspaceWithAll() throws { + let cliPath = try bundledCLIPath() + var environment = ProcessInfo.processInfo.environment + for key in Array(environment.keys) where key.hasPrefix("CMUX_") { + environment.removeValue(forKey: key) + } + environment["CMUX_SOCKET_PATH"] = makeSocketPath("nosocket") + environment["CMUX_CLI_SENTRY_DISABLED"] = "1" + + let result = runProcess( + executablePath: cliPath, + arguments: ["list-surfaces", "--workspace", "workspace:2", "--all"], + environment: environment, + timeout: 5 + ) + + XCTAssertFalse(result.timedOut, result.stderr) + XCTAssertEqual(result.status, 1) + XCTAssertTrue( + result.stderr.contains("list-surfaces: --workspace cannot be combined with --all"), + result.stderr + ) + } + + func testListSurfacesRejectsMissingScopedHandlesBeforeSocket() throws { + let cliPath = try bundledCLIPath() + var environment = ProcessInfo.processInfo.environment + for key in Array(environment.keys) where key.hasPrefix("CMUX_") { + environment.removeValue(forKey: key) + } + environment["CMUX_SOCKET_PATH"] = makeSocketPath("nosocket") + environment["CMUX_CLI_SENTRY_DISABLED"] = "1" + + let workspaceResult = runProcess( + executablePath: cliPath, + arguments: ["list-surfaces", "--workspace", "--json"], + environment: environment, + timeout: 5 + ) + + XCTAssertFalse(workspaceResult.timedOut, workspaceResult.stderr) + XCTAssertEqual(workspaceResult.status, 1) + XCTAssertTrue( + workspaceResult.stderr.contains("list-surfaces requires --workspace "), + workspaceResult.stderr + ) + + let windowResult = runProcess( + executablePath: cliPath, + arguments: ["list-terminals", "--window="], + environment: environment, + timeout: 5 + ) + + XCTAssertFalse(windowResult.timedOut, windowResult.stderr) + XCTAssertEqual(windowResult.status, 1) + XCTAssertTrue( + windowResult.stderr.contains("list-terminals requires --window "), + windowResult.stderr + ) + } + func testSSHSessionListAllWorkspacesReportsQueryErrors() throws { let cliPath = try bundledCLIPath() let socketPath = makeSocketPath("sshlist") diff --git a/cmuxTests/CLINotifyProcessTestSupport.swift b/cmuxTests/CLINotifyProcessTestSupport.swift index a0ae0ffac4c3..9c25122afe81 100644 --- a/cmuxTests/CLINotifyProcessTestSupport.swift +++ b/cmuxTests/CLINotifyProcessTestSupport.swift @@ -477,6 +477,7 @@ extension CLINotifyProcessIntegrationRegressionTests { executablePath: String, arguments: [String], environment: [String: String], + currentDirectoryURL: URL? = nil, standardInput: String? = nil, timeout: TimeInterval ) -> ProcessRunResult { @@ -487,6 +488,7 @@ extension CLINotifyProcessIntegrationRegressionTests { process.executableURL = URL(fileURLWithPath: executablePath) process.arguments = arguments process.environment = environment + process.currentDirectoryURL = currentDirectoryURL process.standardInput = stdinPipe ?? FileHandle.nullDevice process.standardOutput = stdoutPipe process.standardError = stderrPipe diff --git a/docs/cli-contract.md b/docs/cli-contract.md index d7139159fc3f..87e966d4f380 100644 --- a/docs/cli-contract.md +++ b/docs/cli-contract.md @@ -110,6 +110,8 @@ Environment: | `new-split` | Split from a surface in a direction. | | `list-panes` | List panes in a workspace. | | `list-pane-surfaces` | List surfaces in a pane. | +| `list-surfaces` | List surfaces across windows, workspaces, and panes. Defaults to all windows for SSH-friendly inspection. | +| `list-terminals` | List terminal surfaces across windows, workspaces, and panes. Defaults to all windows for SSH-friendly inspection. | | `tree` | Print a window, workspace, pane, and surface tree. | | `top` | Print process/resource usage for cmux windows, workspaces, panes, and surfaces. | | `focus-pane` | Focus a pane. | @@ -493,6 +495,8 @@ the expected text without connecting to a cmux socket. - `cmux new-split --help` -> `Usage: cmux new-split` - `cmux list-panes --help` -> `Usage: cmux list-panes` - `cmux list-pane-surfaces --help` -> `Usage: cmux list-pane-surfaces` +- `cmux list-surfaces --help` -> `Usage: cmux list-surfaces` +- `cmux list-terminals --help` -> `Usage: cmux list-terminals` - `cmux tree --help` -> `Usage: cmux tree` - `cmux top --help` -> `Usage: cmux top` - `cmux focus-pane --help` -> `Usage: cmux focus-pane`