Skip to content

add cmux terminal support for overlay launcher - #35

Merged
umputun merged 3 commits into
umputun:masterfrom
jimmyn:feat/cmux-support
Apr 6, 2026
Merged

umputun merged 3 commits into
umputun:masterfrom
jimmyn:feat/cmux-support

Conversation

@jimmyn

@jimmyn jimmyn commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

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:

  • Detecting cmux via $CMUX_SURFACE_ID env var (set automatically inside cmux terminals)
  • Using the cmux CLI: cmux new-split down to create a split pane, then cmux send --surface <ref> to target the new pane specifically
  • Placing the cmux block before ghostty in the detection chain to prevent the false Ghostty match
  • Closing the split pane after annotations are captured via cmux close-surface

Changes

  • launch-revdiff.sh — cmux block added before ghostty detection
  • launch-plan-review.sh — same cmux block for the planning plugin
  • README, SKILL.md, site/index.html, site/docs.html, llms.txt, install.md, plan-review-hook.py — terminal lists and priority chains updated

Notes

  • cmux's new-split doesn't accept a --command argument (unlike tmux's display-popup or wezterm's split-pane -- cmd). A single cmux send --surface <ref> issued immediately after new-split works 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_SCRIPT replaces the interactive shell so the pane closes automatically when revdiff exits.
  • Surface ref is parsed from cmux new-split output (OK surface:N ...) and used for targeted send and close-surface to avoid affecting the wrong pane.

Test plan

  • Tested in cmux — split opens, revdiff TUI launches, annotations are captured, split closes on quit
  • Verify no regression on ghostty (cmux detection should skip when $CMUX_SURFACE_ID is unset)
  • Verify no regression on tmux/kitty/wezterm (cmux block is after all three)

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.
@jimmyn
jimmyn requested a review from umputun as a code owner April 6, 2026 09:28
jimmyn added 2 commits April 6, 2026 17:13
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 umputun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jimmyn

jimmyn commented Apr 6, 2026

Copy link
Copy Markdown
Contributor Author

Yes, confirmed — cmux docs explicitly state escape sequence handling:

Send text to a terminal surface. Escape sequences: \n and \r send Enter, \t sends Tab.

The built-in example also uses it: cmux send --surface surface:2 "ls -la\n"

Verified with a test: cmux send --surface <ref> "echo NEWLINE_WORKS\n" → command executes, output captured via cmux read-screen.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +107 to +124
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

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +111
# 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"

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
# 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

Copilot uses AI. Check for mistakes.

@umputun umputun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx for confirming. LGTM

@umputun
umputun merged commit 684e982 into umputun:master Apr 6, 2026
5 checks passed
sanchesfree pushed a commit to sanchesfree/revdiff that referenced this pull request Apr 8, 2026
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants