From 3096a654891f36a10b3e455fa3e46c76895e8890 Mon Sep 17 00:00:00 2001 From: kenryu42 Date: Tue, 7 Apr 2026 17:48:02 +0900 Subject: [PATCH 1/6] Prove OMX tmux startup regression before changing compat behavior OMX now probes tmux extended-keys state during startup. Add a regression check that expects show-options/set-option round-tripping so CI can prove current tmux-compat behavior is insufficient before the implementation fix lands. Constraint: Regression-test policy requires a failing test commit before the fix commit Rejected: Ship the implementation without a proving test commit | would violate repo regression policy Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep tmux compatibility coverage focused on observable CLI behavior, not implementation details Tested: python3 -m py_compile tests_v2/test_tmux_compat_matrix.py Not-tested: End-to-end execution against a running cmux instance; local test runs are deferred to CI/VM by repo policy --- tests_v2/test_tmux_compat_matrix.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests_v2/test_tmux_compat_matrix.py b/tests_v2/test_tmux_compat_matrix.py index 59ee3d3ac..1589a5cd8 100644 --- a/tests_v2/test_tmux_compat_matrix.py +++ b/tests_v2/test_tmux_compat_matrix.py @@ -157,6 +157,13 @@ def main() -> int: c.send_surface(s1, f"echo {capture_token}\n") _wait_for(lambda: _surface_has(c, ws, s1, capture_token)) + show_extended_keys = _run_cli(cli, ["show-options", "-sv", "extended-keys"]) + _must(show_extended_keys.stdout.strip() == "off", f"show-options should default extended-keys to off, got {show_extended_keys.stdout!r}") + _run_cli(cli, ["set-option", "-sq", "extended-keys", "always"]) + show_extended_keys = _run_cli(cli, ["show-options", "-sv", "extended-keys"]) + _must(show_extended_keys.stdout.strip() == "always", f"set-option should persist extended-keys, got {show_extended_keys.stdout!r}") + _run_cli(cli, ["set-option", "-sq", "extended-keys", "off"]) + cap = _run_cli(cli, ["capture-pane", "--workspace", ws, "--surface", s1, "--scrollback"]) _must(capture_token in cap.stdout, f"capture-pane missing token: {cap.stdout!r}") From 758c7228e340f253c74fc7e2b2f905dccc9b311d Mon Sep 17 00:00:00 2001 From: kenryu42 Date: Tue, 7 Apr 2026 17:48:10 +0900 Subject: [PATCH 2/6] Keep OMX tmux startup probes from crashing under cmux Teach the tmux compatibility shim to answer the show-options and set-option traffic OMX now uses for its extended-keys lease flow. Persist option state in the existing compat store so startup probes and restoration commands see a consistent value instead of falling through to unsupported-command errors. Constraint: OMX launches through cmux __tmux-compat, not a real tmux server Rejected: Minimal empty-string show-options stub only | fixes the immediate crash but does not model the show/set round-trip OMX performs Confidence: high Scope-risk: narrow Reversibility: clean Directive: If future tmux consumers rely on more options, extend the compat store instead of adding one-off no-op cases Tested: swiftc -parse CLI/cmux.swift Tested: ./scripts/reload.sh --tag fix-omx-extended-keys (blocked by unrelated Ghostty build panic) Not-tested: Full app build and runtime validation; Ghostty submodule setup currently panics on tagged-release version parsing --- CLI/cmux.swift | 87 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 20b729ecb..2ae5d4b20 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -2694,7 +2694,13 @@ struct CMUXCLI { "bind-key", "unbind-key", "copy-mode", + "show-option", + "show-options", + "set", + "set-option", + "set-window-option", "set-buffer", + "setw", "paste-buffer", "list-buffers", "respawn-pane", @@ -11626,7 +11632,7 @@ struct CMUXCLI { print(buffer) } - case "last-window", "next-window", "previous-window", "set-hook", "set-buffer", "list-buffers": + case "last-window", "next-window", "previous-window", "set-hook", "show-option", "show-options", "set", "set-option", "set-window-option", "set-buffer", "list-buffers", "setw": try runTmuxCompatCommand( command: command, commandArgs: rawArgs, @@ -11689,7 +11695,7 @@ struct CMUXCLI { try tmuxPruneCompatWorkspaceState(workspaceId: workspaceId) } - case "set-option", "set", "set-window-option", "setw", "source-file", "refresh-client", "attach-session", "detach-client": + case "source-file", "refresh-client", "attach-session", "detach-client": return case "-V", "-v": @@ -11712,6 +11718,7 @@ struct CMUXCLI { private struct TmuxCompatStore: Codable { var buffers: [String: String] = [:] var hooks: [String: String] = [:] + var options: [String: String] = [:] /// Tracks main-vertical layout state per workspace, keyed by workspace ID. var mainVerticalLayouts: [String: MainVerticalState] = [:] /// Tracks the last surface created by split-window per workspace. @@ -11720,12 +11727,13 @@ struct CMUXCLI { var lastSplitSurface: [String: String] = [:] /// Custom decoder so older store files missing newer keys - /// (mainVerticalLayouts, lastSplitSurface) decode gracefully + /// (options, mainVerticalLayouts, lastSplitSurface) decode gracefully /// instead of throwing and resetting the entire store. init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) buffers = try container.decodeIfPresent([String: String].self, forKey: .buffers) ?? [:] hooks = try container.decodeIfPresent([String: String].self, forKey: .hooks) ?? [:] + options = try container.decodeIfPresent([String: String].self, forKey: .options) ?? [:] mainVerticalLayouts = try container.decodeIfPresent([String: MainVerticalState].self, forKey: .mainVerticalLayouts) ?? [:] lastSplitSurface = try container.decodeIfPresent([String: String].self, forKey: .lastSplitSurface) ?? [:] } @@ -11733,6 +11741,22 @@ struct CMUXCLI { init() {} } + private func tmuxCompatDefaultOptionValue(_ name: String) -> String? { + switch name.lowercased() { + case "extended-keys": + // cmux is not a tmux server, but OMX expects the tmux compatibility + // shim to answer lease-management probes for extended-keys. + return "off" + default: + return nil + } + } + + private func tmuxCompatOptionValue(_ name: String, store: TmuxCompatStore) -> String? { + let normalizedName = name.lowercased() + return store.options[normalizedName] ?? tmuxCompatDefaultOptionValue(normalizedName) + } + private func tmuxCompatStoreURL() -> URL { let homePath = ProcessInfo.processInfo.environment["HOME"] ?? NSString(string: "~").expandingTildeInPath @@ -12204,6 +12228,61 @@ struct CMUXCLI { try saveTmuxCompatStore(store) print("OK") + case "show-option", "show-options": + let parsed = try parseTmuxArguments( + commandArgs, + valueFlags: [], + boolFlags: ["-g", "-p", "-q", "-s", "-v", "-w"] + ) + let store = loadTmuxCompatStore() + guard let optionName = parsed.positional.first? + .trimmingCharacters(in: .whitespacesAndNewlines), + !optionName.isEmpty else { + throw CLIError(message: "\(command) requires an option name") + } + + guard let value = tmuxCompatOptionValue(optionName, store: store) else { + if parsed.hasFlag("-q") { + return + } + throw CLIError(message: "Unsupported tmux compatibility option: \(optionName)") + } + + if parsed.hasFlag("-v") { + print(value) + } else { + print("\(optionName) \(value)") + } + + case "set-option", "set", "set-window-option", "setw": + let parsed = try parseTmuxArguments( + commandArgs, + valueFlags: [], + boolFlags: ["-F", "-a", "-g", "-o", "-p", "-q", "-s", "-u", "-w"] + ) + guard let optionName = parsed.positional.first? + .trimmingCharacters(in: .whitespacesAndNewlines), + !optionName.isEmpty else { + throw CLIError(message: "\(command) requires an option name") + } + + var store = loadTmuxCompatStore() + let normalizedName = optionName.lowercased() + if parsed.hasFlag("-u") { + store.options.removeValue(forKey: normalizedName) + try saveTmuxCompatStore(store) + return + } + + let value = parsed.positional.dropFirst().joined(separator: " ") + .trimmingCharacters(in: .whitespacesAndNewlines) + guard !value.isEmpty else { + throw CLIError(message: "\(command) requires an option value") + } + + store.options[normalizedName] = value + try saveTmuxCompatStore(store) + case "popup": throw CLIError(message: "popup is not supported yet in cmux CLI parity mode") @@ -14393,6 +14472,8 @@ struct CMUXCLI { last-pane [--workspace ] find-window [--content] [--select] clear-history [--workspace ] [--surface ] + show-options [-sv] + set-option [-sq] set-hook [--list] [--unset ] | popup bind-key | unbind-key | copy-mode From 7637ca63aff263f4594a615443685960f299ed8d Mon Sep 17 00:00:00 2001 From: kenryu42 Date: Tue, 7 Apr 2026 18:03:03 +0900 Subject: [PATCH 3/6] Prove remaining tmux option parity gaps before widening compat Add regression coverage for two follow-up tmux-compat behaviors: listing options with bare show-options and honoring set-option -o/-a semantics for stored values. These expectations should fail until the CLI implementation is extended to match them. Constraint: Regression-test policy requires a failing test commit before the fix commit Rejected: Fold the new expectations directly into the implementation commit | would weaken the regression proof required by repo policy Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep tmux option parity checks behavior-focused so future compat rewrites can still reuse them Tested: python3 -m py_compile tests_v2/test_tmux_compat_matrix.py Not-tested: End-to-end execution against a running cmux instance; local test runs are deferred to CI/VM by repo policy --- tests_v2/test_tmux_compat_matrix.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests_v2/test_tmux_compat_matrix.py b/tests_v2/test_tmux_compat_matrix.py index 1589a5cd8..038548665 100644 --- a/tests_v2/test_tmux_compat_matrix.py +++ b/tests_v2/test_tmux_compat_matrix.py @@ -163,6 +163,20 @@ def main() -> int: show_extended_keys = _run_cli(cli, ["show-options", "-sv", "extended-keys"]) _must(show_extended_keys.stdout.strip() == "always", f"set-option should persist extended-keys, got {show_extended_keys.stdout!r}") _run_cli(cli, ["set-option", "-sq", "extended-keys", "off"]) + show_all_options = _run_cli(cli, ["show-options"]) + _must("extended-keys off" in show_all_options.stdout, f"show-options should list default options, got {show_all_options.stdout!r}") + + custom_option = f"@compat_probe_{stamp}" + _run_cli(cli, ["set-option", custom_option, "alpha"]) + _run_cli(cli, ["set-option", "-o", custom_option, "beta"]) + custom_value = _run_cli(cli, ["show-options", "-v", custom_option]) + _must(custom_value.stdout.strip() == "alpha", f"set-option -o should keep the existing value, got {custom_value.stdout!r}") + _run_cli(cli, ["set-option", "-a", custom_option, "gamma"]) + custom_value = _run_cli(cli, ["show-options", "-v", custom_option]) + _must(custom_value.stdout.strip() == "alphagamma", f"set-option -a should append to the existing value, got {custom_value.stdout!r}") + show_all_options = _run_cli(cli, ["show-options"]) + _must(f"{custom_option} alphagamma" in show_all_options.stdout, f"show-options should list custom options, got {show_all_options.stdout!r}") + _run_cli(cli, ["set-option", "-u", custom_option]) cap = _run_cli(cli, ["capture-pane", "--workspace", ws, "--surface", s1, "--scrollback"]) _must(capture_token in cap.stdout, f"capture-pane missing token: {cap.stdout!r}") From e0a1a27f7807689c28029d1c90004063d0eb2d47 Mon Sep 17 00:00:00 2001 From: kenryu42 Date: Tue, 7 Apr 2026 18:03:52 +0900 Subject: [PATCH 4/6] Close the remaining tmux option parity gaps in cmux compat Extend the new tmux option compatibility layer so bare show-options lists known values and set-option now honors the parsed -o and -a flags. This keeps the new behavior internally consistent and resolves the follow-up review notes without changing the OMX-specific happy path. Constraint: Follow-up review fixes must preserve the OMX startup behavior already covered by this branch Rejected: Leave the parsed -o/-a flags as no-ops | would keep newly added compat behavior internally inconsistent Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep tmux option semantics minimal-but-coherent; add broader tmux parity in focused follow-ups if more flags become necessary Tested: swiftc -parse CLI/cmux.swift Tested: python3 -m py_compile tests_v2/test_tmux_compat_matrix.py Not-tested: Full app build and runtime validation; Ghostty submodule setup currently panics on tagged-release version parsing --- CLI/cmux.swift | 73 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 2ae5d4b20..cca93b8b3 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -11741,20 +11741,25 @@ struct CMUXCLI { init() {} } - private func tmuxCompatDefaultOptionValue(_ name: String) -> String? { - switch name.lowercased() { - case "extended-keys": + private func tmuxCompatDefaultOptions() -> [String: String] { + [ // cmux is not a tmux server, but OMX expects the tmux compatibility // shim to answer lease-management probes for extended-keys. - return "off" - default: - return nil - } + "extended-keys": "off" + ] + } + + private func tmuxCompatDefaultOptionValue(_ name: String) -> String? { + tmuxCompatDefaultOptions()[name.lowercased()] + } + + private func tmuxCompatMergedOptions(store: TmuxCompatStore) -> [String: String] { + tmuxCompatDefaultOptions().merging(store.options) { _, stored in stored } } private func tmuxCompatOptionValue(_ name: String, store: TmuxCompatStore) -> String? { let normalizedName = name.lowercased() - return store.options[normalizedName] ?? tmuxCompatDefaultOptionValue(normalizedName) + return tmuxCompatMergedOptions(store: store)[normalizedName] } private func tmuxCompatStoreURL() -> URL { @@ -12235,23 +12240,38 @@ struct CMUXCLI { boolFlags: ["-g", "-p", "-q", "-s", "-v", "-w"] ) let store = loadTmuxCompatStore() - guard let optionName = parsed.positional.first? - .trimmingCharacters(in: .whitespacesAndNewlines), - !optionName.isEmpty else { - throw CLIError(message: "\(command) requires an option name") + let optionName = parsed.positional.first? + .trimmingCharacters(in: .whitespacesAndNewlines) + + if let optionName, !optionName.isEmpty { + guard let value = tmuxCompatOptionValue(optionName, store: store) else { + if parsed.hasFlag("-q") { + return + } + throw CLIError(message: "Unsupported tmux compatibility option: \(optionName)") + } + + if parsed.hasFlag("-v") { + print(value) + } else { + print("\(optionName) \(value)") + } + return } - guard let value = tmuxCompatOptionValue(optionName, store: store) else { + let allOptions = tmuxCompatMergedOptions(store: store) + if allOptions.isEmpty { if parsed.hasFlag("-q") { return } - throw CLIError(message: "Unsupported tmux compatibility option: \(optionName)") + throw CLIError(message: "No tmux compatibility options are set") } - - if parsed.hasFlag("-v") { - print(value) - } else { - print("\(optionName) \(value)") + for (name, value) in allOptions.sorted(by: { $0.key < $1.key }) { + if parsed.hasFlag("-v") { + print(value) + } else { + print("\(name) \(value)") + } } case "set-option", "set", "set-window-option", "setw": @@ -12280,7 +12300,16 @@ struct CMUXCLI { throw CLIError(message: "\(command) requires an option value") } - store.options[normalizedName] = value + if parsed.hasFlag("-o"), tmuxCompatOptionValue(normalizedName, store: store) != nil { + return + } + + if parsed.hasFlag("-a") { + let existing = tmuxCompatOptionValue(normalizedName, store: store) ?? "" + store.options[normalizedName] = existing + value + } else { + store.options[normalizedName] = value + } try saveTmuxCompatStore(store) case "popup": @@ -14472,8 +14501,8 @@ struct CMUXCLI { last-pane [--workspace ] find-window [--content] [--select] clear-history [--workspace ] [--surface ] - show-options [-sv] - set-option [-sq] + show-options [-sv] [name] + set-option [-asq] set-hook [--list] [--unset ] | popup bind-key | unbind-key | copy-mode From 15cd50ef988f0c46d720f3c18ab5bd85e7c75c62 Mon Sep 17 00:00:00 2001 From: kenryu42 Date: Tue, 7 Apr 2026 18:40:23 +0900 Subject: [PATCH 5/6] Prove help and exact-value tmux option regressions before fixing them Add behavioral coverage for two remaining tmux-compat gaps on this branch: new show/set subcommands should surface help via --help, and set-option should preserve explicit empty strings plus leading or trailing whitespace in stored values. Constraint: Regression-test policy requires a failing test commit before the fix commit Rejected: Bundle these assertions into the implementation commit | would weaken the regression proof required by repo policy Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep tmux-compat regression cases focused on user-visible CLI behavior rather than implementation details Tested: python3 -m py_compile tests_v2/test_tmux_compat_matrix.py Not-tested: End-to-end execution against a running cmux instance; local test runs are deferred to CI/VM by repo policy --- tests_v2/test_tmux_compat_matrix.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests_v2/test_tmux_compat_matrix.py b/tests_v2/test_tmux_compat_matrix.py index 038548665..3ec4a9314 100644 --- a/tests_v2/test_tmux_compat_matrix.py +++ b/tests_v2/test_tmux_compat_matrix.py @@ -178,6 +178,24 @@ def main() -> int: _must(f"{custom_option} alphagamma" in show_all_options.stdout, f"show-options should list custom options, got {show_all_options.stdout!r}") _run_cli(cli, ["set-option", "-u", custom_option]) + show_help = _run_cli(cli, ["show-options", "--help"]) + _must("Usage: cmux show-options" in show_help.stdout, f"show-options --help should print usage, got {show_help.stdout!r}") + set_help = _run_cli(cli, ["set-option", "--help"]) + _must("Usage: cmux set-option" in set_help.stdout, f"set-option --help should print usage, got {set_help.stdout!r}") + + empty_option = f"@empty_probe_{stamp}" + _run_cli(cli, ["set-option", empty_option, ""]) + empty_value = _run_cli(cli, ["show-options", "-v", empty_option]) + _must(empty_value.stdout == "\n", f"set-option should preserve explicit empty values, got {empty_value.stdout!r}") + _run_cli(cli, ["set-option", "-u", empty_option]) + + spaced_option = f"@space_probe_{stamp}" + spaced_value = " padded value " + _run_cli(cli, ["set-option", spaced_option, spaced_value]) + shown_spaced_value = _run_cli(cli, ["show-options", "-v", spaced_option]) + _must(shown_spaced_value.stdout == spaced_value + "\n", f"set-option should preserve leading/trailing whitespace, got {shown_spaced_value.stdout!r}") + _run_cli(cli, ["set-option", "-u", spaced_option]) + cap = _run_cli(cli, ["capture-pane", "--workspace", ws, "--surface", s1, "--scrollback"]) _must(capture_token in cap.stdout, f"capture-pane missing token: {cap.stdout!r}") From 912beacd81ebe9e7a95d3bef7a5747eb8202c69c Mon Sep 17 00:00:00 2001 From: kenryu42 Date: Tue, 7 Apr 2026 18:41:45 +0900 Subject: [PATCH 6/6] Preserve tmux option values and surface help for the new compat commands Close the remaining review feedback on this branch by wiring show/set tmux-compat commands into subcommand help and by storing option values exactly as passed. That keeps quoted empty strings and whitespace-significant values round-trippable while making Unknown command 'show-options'. Run 'cmux help' to see available commands. and friends behave like other CLI subcommands. Constraint: The fix must preserve the OMX startup behavior already covered by this branch Rejected: Trim and reject empty option values | would keep the new tmux-compat layer unable to round-trip exact values Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat tmux-compat option values as exact user input unless tmux semantics require transformation Tested: swiftc -parse CLI/cmux.swift Tested: python3 -m py_compile tests_v2/test_tmux_compat_matrix.py Tested: ./scripts/reload.sh --tag fix-tmux-option-help (blocked by unrelated Ghostty build panic) Not-tested: Full app build and runtime validation; Ghostty submodule setup currently panics on tagged-release version parsing --- CLI/cmux.swift | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index cca93b8b3..ca0cc7408 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -6877,6 +6877,37 @@ struct CMUXCLI { cmux omc team 3:claude "implement feature" cmux omc --watch """) + case "show-option", "show-options": + return """ + Usage: cmux show-options [-svq] [name] + + Show tmux-compat option values. Without a name, list all known values. + + Flags: + -s Server option scope compatibility flag + -v Print only the option value + -q Suppress errors for missing or unsupported options + + Examples: + cmux show-options + cmux show-options -sv extended-keys + """ + case "set", "set-option", "set-window-option", "setw": + return """ + Usage: cmux set-option [-aouq] + + Persist a tmux-compat option value. + + Flags: + -a Append to the existing value + -o Only set if the option is not already set + -u Unset the option + -q Suppress errors for unknown options + + Examples: + cmux set-option extended-keys always + cmux set-option -u extended-keys + """ case "identify": return """ Usage: cmux identify [--workspace ] [--surface ] [--no-caller] @@ -12294,11 +12325,11 @@ struct CMUXCLI { return } - let value = parsed.positional.dropFirst().joined(separator: " ") - .trimmingCharacters(in: .whitespacesAndNewlines) - guard !value.isEmpty else { + let valueParts = Array(parsed.positional.dropFirst()) + guard !valueParts.isEmpty else { throw CLIError(message: "\(command) requires an option value") } + let value = valueParts.joined(separator: " ") if parsed.hasFlag("-o"), tmuxCompatOptionValue(normalizedName, store: store) != nil { return