add cmux terminal support for overlay launcher - #35
Conversation
cmux (https://cmux.com) is a native macOS terminal for AI coding agents built on libghostty. It sets TERM_PROGRAM=ghostty, which causes the existing Ghostty AppleScript path to fail. Detect cmux first via $CMUX_SURFACE_ID and use its CLI API (new-split + send --surface) instead of AppleScript.
Instead of a fixed sleep 0.5 before sending commands to the new split, use cmux wait-for as a sync primitive: retry a compound signal-and-exec command until the shell processes it. Adapts to any shell startup time.
The pty input buffer holds text until the shell reads it, so a single cmux send immediately after new-split is sufficient. No retry loop, no wait-for synchronization, no visual noise in the split pane.
umputun
left a comment
There was a problem hiding this comment.
LGTM. clean implementation, correctly placed before ghostty to avoid false match. both launcher scripts updated consistently, docs are thorough.
one thing I'm not 100% sure about - does cmux send interpret \n as a newline? i.e., cmux send --surface "$CMUX_SURF" "exec $LAUNCH_SCRIPT\n" - if \n is passed literally the command won't execute. you tested it and it works, so I assume it does, just want to confirm.
|
Yes, confirmed — cmux docs explicitly state escape sequence handling: The built-in example also uses it: Verified with a test: |
There was a problem hiding this comment.
Pull request overview
Adds support for running the revdiff overlay inside cmux terminals (which set TERM_PROGRAM=ghostty but don’t expose Ghostty’s AppleScript API), by detecting cmux via $CMUX_SURFACE_ID and launching revdiff via the cmux CLI.
Changes:
- Add cmux detection/launch logic to both revdiff overlay launchers (main + planning).
- Update terminal support docs and priority chain to include cmux before ghostty.
- Update plugin skill/reference text to reflect cmux support.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
.claude-plugin/skills/revdiff/scripts/launch-revdiff.sh |
Adds cmux split + targeted send + close logic before Ghostty detection. |
plugins/revdiff-planning/scripts/launch-plan-review.sh |
Mirrors cmux overlay logic for the planning plugin. |
README.md |
Documents cmux as a supported overlay terminal and updates priority order. |
.claude-plugin/skills/revdiff/SKILL.md |
Updates skill metadata/docs to include cmux in supported terminals and priority chain. |
.claude-plugin/skills/revdiff/references/install.md |
Updates install/use docs to include cmux in terminal list. |
plugins/revdiff-planning/scripts/plan-review-hook.py |
Updates requirement text to include cmux. |
site/index.html |
Adds cmux terminal card and updates priority text on the website landing page. |
site/docs.html |
Updates terminal support table and detection priority chain to include cmux. |
llms.txt |
Updates supported terminal overlay list to include cmux. |
site/llms.txt |
Same llms.txt update for the site copy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| CMUX_NEW=$(cmux new-split down 2>&1) || true | ||
| CMUX_SURF=$(echo "$CMUX_NEW" | grep -o 'surface:[0-9]*' | head -1) | ||
|
|
||
| # send exec command immediately — the pty input buffer holds the text | ||
| # until the new pane's shell finishes initializing and reads it | ||
| if [ -n "$CMUX_SURF" ]; then | ||
| cmux send --surface "$CMUX_SURF" "exec $LAUNCH_SCRIPT\n" | ||
| else | ||
| cmux send "exec $LAUNCH_SCRIPT\n" | ||
| fi | ||
|
|
||
| while [ ! -f "$SENTINEL" ]; do | ||
| sleep 0.3 | ||
| done | ||
| # close the split pane | ||
| if [ -n "$CMUX_SURF" ]; then | ||
| cmux close-surface --surface "$CMUX_SURF" 2>/dev/null || true | ||
| fi |
There was a problem hiding this comment.
The cmux path ignores failures from cmux new-split (due to || true) and falls back to untargeted cmux send when the surface ref can't be parsed. This can end up sending exec ... to the wrong surface (potentially the invoking pane) and then blocking forever in the sentinel wait. Consider failing fast if new-split fails or if CMUX_SURF is empty, and add a bounded timeout/cleanup path for the sentinel loop (closing the created surface on error when possible).
| CMUX_NEW=$(cmux new-split down 2>&1) || true | |
| CMUX_SURF=$(echo "$CMUX_NEW" | grep -o 'surface:[0-9]*' | head -1) | |
| # send exec command immediately — the pty input buffer holds the text | |
| # until the new pane's shell finishes initializing and reads it | |
| if [ -n "$CMUX_SURF" ]; then | |
| cmux send --surface "$CMUX_SURF" "exec $LAUNCH_SCRIPT\n" | |
| else | |
| cmux send "exec $LAUNCH_SCRIPT\n" | |
| fi | |
| while [ ! -f "$SENTINEL" ]; do | |
| sleep 0.3 | |
| done | |
| # close the split pane | |
| if [ -n "$CMUX_SURF" ]; then | |
| cmux close-surface --surface "$CMUX_SURF" 2>/dev/null || true | |
| fi | |
| if ! CMUX_NEW=$(cmux new-split down 2>&1); then | |
| echo "error: cmux new-split failed" >&2 | |
| rm -f "$SENTINEL" "$LAUNCH_SCRIPT" | |
| exit 1 | |
| fi | |
| CMUX_SURF=$(echo "$CMUX_NEW" | grep -o 'surface:[0-9]*' | head -1) | |
| if [ -z "$CMUX_SURF" ]; then | |
| echo "error: failed to parse cmux surface from new-split output: $CMUX_NEW" >&2 | |
| rm -f "$SENTINEL" "$LAUNCH_SCRIPT" | |
| exit 1 | |
| fi | |
| # send exec command immediately — the pty input buffer holds the text | |
| # until the new pane's shell finishes initializing and reads it | |
| if ! cmux send --surface "$CMUX_SURF" "exec $LAUNCH_SCRIPT\n"; then | |
| echo "error: failed to send launch command to cmux surface $CMUX_SURF" >&2 | |
| cmux close-surface --surface "$CMUX_SURF" 2>/dev/null || true | |
| rm -f "$SENTINEL" "$LAUNCH_SCRIPT" | |
| exit 1 | |
| fi | |
| CMUX_WAIT_TIMEOUT=60 | |
| CMUX_WAIT_ELAPSED=0 | |
| while [ ! -f "$SENTINEL" ]; do | |
| if [ "$CMUX_WAIT_ELAPSED" -ge "$CMUX_WAIT_TIMEOUT" ]; then | |
| echo "error: timed out waiting for revdiff to finish in cmux surface $CMUX_SURF" >&2 | |
| cmux close-surface --surface "$CMUX_SURF" 2>/dev/null || true | |
| rm -f "$SENTINEL" "$LAUNCH_SCRIPT" | |
| exit 1 | |
| fi | |
| sleep 1 | |
| CMUX_WAIT_ELAPSED=$((CMUX_WAIT_ELAPSED + 1)) | |
| done | |
| # close the split pane | |
| cmux close-surface --surface "$CMUX_SURF" 2>/dev/null || true |
| # cmux: split pane via cmux CLI (must precede ghostty — cmux also sets TERM_PROGRAM=ghostty) | ||
| if [ -n "${CMUX_SURFACE_ID:-}" ] && command -v cmux >/dev/null 2>&1; then | ||
| SENTINEL=$(mktemp /tmp/plan-review-done-XXXXXX) | ||
| rm -f "$SENTINEL" | ||
|
|
||
| LAUNCH_SCRIPT=$(mktemp /tmp/plan-review-launch-XXXXXX.sh) | ||
| trap 'rm -f "$OUTPUT_FILE" "$SENTINEL" "$LAUNCH_SCRIPT"' EXIT | ||
| cat > "$LAUNCH_SCRIPT" <<LAUNCHER | ||
| #!/bin/sh | ||
| $REVDIFF_CMD; touch '$SENTINEL' | ||
| LAUNCHER | ||
| chmod +x "$LAUNCH_SCRIPT" | ||
|
|
||
| CMUX_NEW=$(cmux new-split down 2>&1) || true | ||
| CMUX_SURF=$(echo "$CMUX_NEW" | grep -o 'surface:[0-9]*' | head -1) | ||
|
|
||
| # send exec command immediately — the pty input buffer holds the text | ||
| # until the new pane's shell finishes initializing and reads it | ||
| if [ -n "$CMUX_SURF" ]; then | ||
| cmux send --surface "$CMUX_SURF" "exec $LAUNCH_SCRIPT\n" | ||
| else | ||
| cmux send "exec $LAUNCH_SCRIPT\n" | ||
| fi | ||
|
|
||
| while [ ! -f "$SENTINEL" ]; do | ||
| sleep 0.3 | ||
| done | ||
| if [ -n "$CMUX_SURF" ]; then | ||
| cmux close-surface --surface "$CMUX_SURF" 2>/dev/null || true | ||
| fi | ||
| rm -f "$SENTINEL" "$LAUNCH_SCRIPT" |
There was a problem hiding this comment.
The cmux path ignores failures from cmux new-split (due to || true) and falls back to untargeted cmux send when the surface ref can't be parsed. This can send exec ... to the wrong pane and then block forever in the sentinel wait. Consider checking new-split exit status / expected output, requiring a parsed CMUX_SURF before sending, and adding a timeout + cleanup path for the sentinel loop (including close-surface if a surface was created).
| # cmux: split pane via cmux CLI (must precede ghostty — cmux also sets TERM_PROGRAM=ghostty) | |
| if [ -n "${CMUX_SURFACE_ID:-}" ] && command -v cmux >/dev/null 2>&1; then | |
| SENTINEL=$(mktemp /tmp/plan-review-done-XXXXXX) | |
| rm -f "$SENTINEL" | |
| LAUNCH_SCRIPT=$(mktemp /tmp/plan-review-launch-XXXXXX.sh) | |
| trap 'rm -f "$OUTPUT_FILE" "$SENTINEL" "$LAUNCH_SCRIPT"' EXIT | |
| cat > "$LAUNCH_SCRIPT" <<LAUNCHER | |
| #!/bin/sh | |
| $REVDIFF_CMD; touch '$SENTINEL' | |
| LAUNCHER | |
| chmod +x "$LAUNCH_SCRIPT" | |
| CMUX_NEW=$(cmux new-split down 2>&1) || true | |
| CMUX_SURF=$(echo "$CMUX_NEW" | grep -o 'surface:[0-9]*' | head -1) | |
| # send exec command immediately — the pty input buffer holds the text | |
| # until the new pane's shell finishes initializing and reads it | |
| if [ -n "$CMUX_SURF" ]; then | |
| cmux send --surface "$CMUX_SURF" "exec $LAUNCH_SCRIPT\n" | |
| else | |
| cmux send "exec $LAUNCH_SCRIPT\n" | |
| fi | |
| while [ ! -f "$SENTINEL" ]; do | |
| sleep 0.3 | |
| done | |
| if [ -n "$CMUX_SURF" ]; then | |
| cmux close-surface --surface "$CMUX_SURF" 2>/dev/null || true | |
| fi | |
| rm -f "$SENTINEL" "$LAUNCH_SCRIPT" | |
| cleanup_cmux_launch() { | |
| rm -f "${OUTPUT_FILE:-}" "${SENTINEL:-}" "${LAUNCH_SCRIPT:-}" | |
| if [ -n "${CMUX_SURF:-}" ]; then | |
| cmux close-surface --surface "$CMUX_SURF" 2>/dev/null || true | |
| CMUX_SURF="" | |
| fi | |
| } | |
| # cmux: split pane via cmux CLI (must precede ghostty — cmux also sets TERM_PROGRAM=ghostty) | |
| if [ -n "${CMUX_SURFACE_ID:-}" ] && command -v cmux >/dev/null 2>&1; then | |
| SENTINEL=$(mktemp /tmp/plan-review-done-XXXXXX) | |
| rm -f "$SENTINEL" | |
| LAUNCH_SCRIPT=$(mktemp /tmp/plan-review-launch-XXXXXX.sh) | |
| CMUX_SURF="" | |
| trap 'cleanup_cmux_launch' EXIT | |
| cat > "$LAUNCH_SCRIPT" <<LAUNCHER | |
| #!/bin/sh | |
| $REVDIFF_CMD; touch '$SENTINEL' | |
| LAUNCHER | |
| chmod +x "$LAUNCH_SCRIPT" | |
| if ! CMUX_NEW=$(cmux new-split down 2>&1); then | |
| echo "error: cmux new-split failed: $CMUX_NEW" >&2 | |
| exit 1 | |
| fi | |
| CMUX_SURF=$(echo "$CMUX_NEW" | grep -o 'surface:[0-9]*' | head -1) | |
| if [ -z "$CMUX_SURF" ]; then | |
| echo "error: cmux new-split did not return a surface reference: $CMUX_NEW" >&2 | |
| exit 1 | |
| fi | |
| # send exec command immediately — the pty input buffer holds the text | |
| # until the new pane's shell finishes initializing and reads it | |
| if ! cmux send --surface "$CMUX_SURF" "exec $LAUNCH_SCRIPT\n"; then | |
| echo "error: failed to send launch command to cmux surface $CMUX_SURF" >&2 | |
| exit 1 | |
| fi | |
| CMUX_WAIT_TIMEOUT=60 | |
| CMUX_WAIT_START=$SECONDS | |
| while [ ! -f "$SENTINEL" ]; do | |
| if [ $((SECONDS - CMUX_WAIT_START)) -ge "$CMUX_WAIT_TIMEOUT" ]; then | |
| echo "error: timed out waiting for cmux launch to finish" >&2 | |
| exit 1 | |
| fi | |
| sleep 0.3 | |
| done | |
| cleanup_cmux_launch | |
| trap - EXIT |
* add cmux terminal support for overlay launcher cmux (https://cmux.com) is a native macOS terminal for AI coding agents built on libghostty. It sets TERM_PROGRAM=ghostty, which causes the existing Ghostty AppleScript path to fail. Detect cmux first via $CMUX_SURFACE_ID and use its CLI API (new-split + send --surface) instead of AppleScript. * replace sleep with wait-for synchronization for shell readiness Instead of a fixed sleep 0.5 before sending commands to the new split, use cmux wait-for as a sync primitive: retry a compound signal-and-exec command until the shell processes it. Adapts to any shell startup time. * simplify: single send instead of retry loop The pty input buffer holds text until the shell reads it, so a single cmux send immediately after new-split is sufficient. No retry loop, no wait-for synchronization, no visual noise in the split pane.
Summary
cmux is a native macOS terminal built for AI coding agents. It's based on libghostty and sets
TERM_PROGRAM=ghostty, which causes the existing Ghostty AppleScript detection path to fail (cmux doesn't expose Ghostty's AppleScript API).This PR adds cmux as a supported terminal overlay by:
$CMUX_SURFACE_IDenv var (set automatically inside cmux terminals)cmux new-split downto create a split pane, thencmux send --surface <ref>to target the new pane specificallycmux close-surfaceChanges
launch-revdiff.sh— cmux block added before ghostty detectionlaunch-plan-review.sh— same cmux block for the planning pluginNotes
new-splitdoesn't accept a--commandargument (unlike tmux'sdisplay-popupor wezterm'ssplit-pane -- cmd). A singlecmux send --surface <ref>issued immediately afternew-splitworks because the pty input buffer holds the text until the new pane's shell finishes initializing and reads it. No sleep or retry needed.exec $LAUNCH_SCRIPTreplaces the interactive shell so the pane closes automatically when revdiff exits.cmux new-splitoutput (OK surface:N ...) and used for targetedsendandclose-surfaceto avoid affecting the wrong pane.Test plan
$CMUX_SURFACE_IDis unset)