From fb35f3bc399be028ac3505ce0d4cfd854d7dfeeb Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:33:04 -0800 Subject: [PATCH] feat: enable ssh shell niceties by default --- Resources/shell-integration/.zshenv | 15 +++- .../test_ssh_shell_integration_features.py | 74 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tests_v2/test_ssh_shell_integration_features.py diff --git a/Resources/shell-integration/.zshenv b/Resources/shell-integration/.zshenv index 21570241..6f6a5b69 100644 --- a/Resources/shell-integration/.zshenv +++ b/Resources/shell-integration/.zshenv @@ -29,6 +29,19 @@ fi [[ ! -r "$_cmux_file" ]] || builtin source -- "$_cmux_file" } always { if [[ -o interactive ]]; then + # Opt into Ghostty's SSH shell niceties by default so plain `ssh ...` + # gets TERM compatibility + remote terminfo setup. + if [[ -n "${GHOSTTY_SHELL_FEATURES:-}" ]]; then + builtin typeset _cmux_features=",$GHOSTTY_SHELL_FEATURES," + if [[ "$_cmux_features" != *",ssh-env,"* ]]; then + builtin export GHOSTTY_SHELL_FEATURES="${GHOSTTY_SHELL_FEATURES},ssh-env" + _cmux_features=",$GHOSTTY_SHELL_FEATURES," + fi + if [[ "$_cmux_features" != *",ssh-terminfo,"* ]]; then + builtin export GHOSTTY_SHELL_FEATURES="${GHOSTTY_SHELL_FEATURES},ssh-terminfo" + fi + fi + # We overwrote GhosttyKit's injected ZDOTDIR, so manually load Ghostty's # zsh integration if available. if [[ -n "${GHOSTTY_RESOURCES_DIR:-}" ]]; then @@ -43,5 +56,5 @@ fi fi fi - builtin unset _cmux_file _cmux_ghostty _cmux_integ + builtin unset _cmux_file _cmux_features _cmux_ghostty _cmux_integ } diff --git a/tests_v2/test_ssh_shell_integration_features.py b/tests_v2/test_ssh_shell_integration_features.py new file mode 100644 index 00000000..6bde143c --- /dev/null +++ b/tests_v2/test_ssh_shell_integration_features.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Regression: cmux shell integration enables ssh niceties by default.""" + +import os +import shlex +import tempfile +import time +from pathlib import Path + +import sys +sys.path.insert(0, str(Path(__file__).parent)) +from cmux import cmux, cmuxError + + +SOCKET_PATH = os.environ.get("CMUX_SOCKET", "/tmp/cmux-debug.sock") + + +def _must(cond: bool, msg: str) -> None: + if not cond: + raise cmuxError(msg) + + +def _wait_for_surface(client: cmux, workspace_id: str, timeout_s: float = 8.0) -> str: + deadline = time.time() + timeout_s + while time.time() < deadline: + surfaces = client.list_surfaces(workspace_id) + if surfaces: + return str(surfaces[0][1]) + time.sleep(0.1) + raise cmuxError(f"workspace {workspace_id} did not create a terminal surface in time") + + +def main() -> int: + output_path = Path(tempfile.gettempdir()) / f"cmux_ssh_shell_features_{os.getpid()}_{int(time.time() * 1000)}.txt" + workspace_id = "" + + with cmux(SOCKET_PATH) as client: + try: + workspace_id = client.new_workspace() + surface_id = _wait_for_surface(client, workspace_id) + + probe = f"echo \"$GHOSTTY_SHELL_FEATURES\" > {shlex.quote(str(output_path))}\n" + deadline = time.time() + 8.0 + last_send = 0.0 + while time.time() < deadline and not output_path.exists(): + now = time.time() + # Surface creation can race the first shell prompt; retry until one sticks. + if now - last_send >= 0.5: + client.send_surface(surface_id, probe) + last_send = now + time.sleep(0.05) + _must(output_path.exists(), "Timed out waiting for shell feature probe output") + + raw = output_path.read_text(encoding="utf-8", errors="replace").strip() + features = {token.strip() for token in raw.split(",") if token.strip()} + _must("ssh-env" in features, f"GHOSTTY_SHELL_FEATURES missing ssh-env: {raw!r}") + _must("ssh-terminfo" in features, f"GHOSTTY_SHELL_FEATURES missing ssh-terminfo: {raw!r}") + finally: + if workspace_id: + try: + client.close_workspace(workspace_id) + except Exception: + pass + try: + output_path.unlink() + except FileNotFoundError: + pass + + print("PASS: shell integration defaults include ssh-env and ssh-terminfo") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())