From 6f05e96e675f71da2ef811d4a1b2e015740c4581 Mon Sep 17 00:00:00 2001 From: edlsh Date: Fri, 17 Apr 2026 15:28:35 -0400 Subject: [PATCH] fix(tmux): respect pane-base-index in setup_tmux_session When a user's ~/.tmux.conf sets `setw -g pane-base-index 1` (common in popular dotfiles and Oh My Zsh setups), tmux panes are numbered starting at 1, not 0. Ralph previously handled `base-index` (windows) but hardcoded pane targets as .0/.1/.2. With pane-base-index=1: - send-keys -t session:0.0 targets a non-existent pane (silently fails) - The Ralph loop never starts in the left pane - The live-log tail and ralph-monitor land in the wrong panes Visible symptom: running `ralph --monitor` opens a tmux session with two empty shell prompts and a stray `tail -f` in a third pane. The loop never runs. Fix: add `get_tmux_pane_base_index` mirroring `get_tmux_base_index`, then compute pane indices as `base_pane + {0,1,2}` throughout `setup_tmux_session`. Default behavior (pane-base-index=0) is unchanged. Tests (test_tmux_integration.bats): - Updated inline mirror of setup_tmux_session to match - Parameterised the tmux show-options mock to return per-option values via MOCK_TMUX_BASE_INDEX / MOCK_TMUX_PANE_BASE_INDEX - Added 3 regression tests: * pane-base-index 1 targets panes .1/.2/.3 (no .0 touches) * base-index 1 + pane-base-index 1 targets 1.1/1.2/1.3 * get_tmux_pane_base_index defaults to 0 All 680 tests pass. --- ralph_loop.sh | 43 ++++--- tests/integration/test_tmux_integration.bats | 117 ++++++++++++++++--- 2 files changed, 131 insertions(+), 29 deletions(-) diff --git a/ralph_loop.sh b/ralph_loop.sh index cd094e81..64bf3e3e 100755 --- a/ralph_loop.sh +++ b/ralph_loop.sh @@ -274,15 +274,32 @@ get_tmux_base_index() { echo "${base_index:-0}" } +# Get the tmux pane-base-index (handles custom tmux configurations) +# Returns: the base pane index (typically 0 or 1) +# Many popular dotfiles set `setw -g pane-base-index 1` so panes number from 1. +get_tmux_pane_base_index() { + local pane_base_index + pane_base_index=$(tmux show-options -gwv pane-base-index 2>/dev/null) + # Default to 0 if not set or tmux command fails + echo "${pane_base_index:-0}" +} + # Setup tmux session with monitor setup_tmux_session() { local session_name="ralph-$(date +%s)" local ralph_home="${RALPH_HOME:-$HOME/.ralph}" local project_dir="$(pwd)" - # Get the tmux base-index to handle custom configurations (e.g., base-index 1) - local base_win + # Get the tmux base-index / pane-base-index to handle custom configurations + # (e.g. `set -g base-index 1`, `setw -g pane-base-index 1` — very common in + # dotfiles). Without this, pane targets like `.0` don't exist and the Ralph + # loop never starts, leaving empty panes. See: tmux pane-base-index. + local base_win base_pane base_win=$(get_tmux_base_index) + base_pane=$(get_tmux_pane_base_index) + local pane0=$((base_pane + 0)) # Ralph loop (left) + local pane1=$((base_pane + 1)) # Claude output (right-top) + local pane2=$((base_pane + 2)) # Status monitor (right-bottom) log_status "INFO" "Setting up tmux session: $session_name" @@ -296,16 +313,16 @@ setup_tmux_session() { tmux split-window -h -t "$session_name" -c "$project_dir" # Split right pane horizontally (top: Claude output, bottom: status) - tmux split-window -v -t "$session_name:${base_win}.1" -c "$project_dir" + tmux split-window -v -t "$session_name:${base_win}.${pane1}" -c "$project_dir" - # Right-top pane (pane 1): Live Claude Code output - tmux send-keys -t "$session_name:${base_win}.1" "tail -f '$project_dir/$LIVE_LOG_FILE'" Enter + # Right-top pane: Live Claude Code output + tmux send-keys -t "$session_name:${base_win}.${pane1}" "tail -f '$project_dir/$LIVE_LOG_FILE'" Enter - # Right-bottom pane (pane 2): Ralph status monitor + # Right-bottom pane: Ralph status monitor if command -v ralph-monitor &> /dev/null; then - tmux send-keys -t "$session_name:${base_win}.2" "ralph-monitor" Enter + tmux send-keys -t "$session_name:${base_win}.${pane2}" "ralph-monitor" Enter else - tmux send-keys -t "$session_name:${base_win}.2" "'$ralph_home/ralph_monitor.sh'" Enter + tmux send-keys -t "$session_name:${base_win}.${pane2}" "'$ralph_home/ralph_monitor.sh'" Enter fi # Start ralph loop in the left pane (exclude tmux flag to avoid recursion) @@ -367,15 +384,15 @@ setup_tmux_session() { # circuit breaker, error, or manual interrupt). Without this, the # tail -f and ralph_monitor.sh panes keep the session alive forever. # Issue: https://github.com/frankbria/ralph-claude-code/issues/176 - tmux send-keys -t "$session_name:${base_win}.0" "$ralph_cmd; tmux kill-session -t $session_name 2>/dev/null" Enter + tmux send-keys -t "$session_name:${base_win}.${pane0}" "$ralph_cmd; tmux kill-session -t $session_name 2>/dev/null" Enter # Focus on left pane (main ralph loop) - tmux select-pane -t "$session_name:${base_win}.0" + tmux select-pane -t "$session_name:${base_win}.${pane0}" # Set pane titles (requires tmux 2.6+) - tmux select-pane -t "$session_name:${base_win}.0" -T "Ralph Loop" - tmux select-pane -t "$session_name:${base_win}.1" -T "Claude Output" - tmux select-pane -t "$session_name:${base_win}.2" -T "Status" + tmux select-pane -t "$session_name:${base_win}.${pane0}" -T "Ralph Loop" + tmux select-pane -t "$session_name:${base_win}.${pane1}" -T "Claude Output" + tmux select-pane -t "$session_name:${base_win}.${pane2}" -T "Status" # Set window title tmux rename-window -t "$session_name:${base_win}" "Ralph: Loop | Output | Status" diff --git a/tests/integration/test_tmux_integration.bats b/tests/integration/test_tmux_integration.bats index 5da662e8..354b8318 100644 --- a/tests/integration/test_tmux_integration.bats +++ b/tests/integration/test_tmux_integration.bats @@ -46,15 +46,28 @@ get_tmux_base_index() { echo "${base_index:-0}" } +# Get the tmux pane-base-index (handles custom tmux configurations) +# Returns: the base pane index (typically 0 or 1) +get_tmux_pane_base_index() { + local pane_base_index + pane_base_index=$(tmux show-options -gwv pane-base-index 2>/dev/null) + # Default to 0 if not set or tmux command fails + echo "${pane_base_index:-0}" +} + # Setup tmux session with monitor setup_tmux_session() { local session_name="ralph-$(date +%s)" local ralph_home="${RALPH_HOME:-$HOME/.ralph}" local project_dir="$(pwd)" - # Get the tmux base-index to handle custom configurations (e.g., base-index 1) - local base_win + # Get the tmux base-index / pane-base-index to handle custom configurations + local base_win base_pane base_win=$(get_tmux_base_index) + base_pane=$(get_tmux_pane_base_index) + local pane0=$((base_pane + 0)) + local pane1=$((base_pane + 1)) + local pane2=$((base_pane + 2)) log_status "INFO" "Setting up tmux session: $session_name" @@ -68,16 +81,16 @@ setup_tmux_session() { tmux split-window -h -t "$session_name" -c "$project_dir" # Split right pane horizontally (top: Claude output, bottom: status) - tmux split-window -v -t "$session_name:${base_win}.1" -c "$project_dir" + tmux split-window -v -t "$session_name:${base_win}.${pane1}" -c "$project_dir" - # Right-top pane (pane 1): Live Claude Code output - tmux send-keys -t "$session_name:${base_win}.1" "tail -f '$project_dir/$LIVE_LOG_FILE'" Enter + # Right-top pane: Live Claude Code output + tmux send-keys -t "$session_name:${base_win}.${pane1}" "tail -f '$project_dir/$LIVE_LOG_FILE'" Enter - # Right-bottom pane (pane 2): Ralph status monitor + # Right-bottom pane: Ralph status monitor if command -v ralph-monitor &> /dev/null; then - tmux send-keys -t "$session_name:${base_win}.2" "ralph-monitor" Enter + tmux send-keys -t "$session_name:${base_win}.${pane2}" "ralph-monitor" Enter else - tmux send-keys -t "$session_name:${base_win}.2" "'$ralph_home/ralph_monitor.sh'" Enter + tmux send-keys -t "$session_name:${base_win}.${pane2}" "'$ralph_home/ralph_monitor.sh'" Enter fi # Start ralph loop in the left pane (exclude tmux flag to avoid recursion) @@ -132,15 +145,15 @@ setup_tmux_session() { ralph_cmd="$ralph_cmd --backup" fi - tmux send-keys -t "$session_name:${base_win}.0" "$ralph_cmd; tmux kill-session -t $session_name 2>/dev/null" Enter + tmux send-keys -t "$session_name:${base_win}.${pane0}" "$ralph_cmd; tmux kill-session -t $session_name 2>/dev/null" Enter # Focus on left pane (main ralph loop) - tmux select-pane -t "$session_name:${base_win}.0" + tmux select-pane -t "$session_name:${base_win}.${pane0}" # Set pane titles - tmux select-pane -t "$session_name:${base_win}.0" -T "Ralph Loop" - tmux select-pane -t "$session_name:${base_win}.1" -T "Claude Output" - tmux select-pane -t "$session_name:${base_win}.2" -T "Status" + tmux select-pane -t "$session_name:${base_win}.${pane0}" -T "Ralph Loop" + tmux select-pane -t "$session_name:${base_win}.${pane1}" -T "Claude Output" + tmux select-pane -t "$session_name:${base_win}.${pane2}" -T "Status" # Set window title tmux rename-window -t "$session_name:${base_win}" "Ralph: Loop | Output | Status" @@ -189,7 +202,10 @@ setup() { # Tracking tmux mock: records every invocation to $TMUX_CALL_LOG # attach-session returns 0 (does NOT exit) so tests survive the exit 0 in setup_tmux_session - # show-options returns "0" for get_tmux_base_index + # show-options returns value from MOCK_TMUX_BASE_INDEX / MOCK_TMUX_PANE_BASE_INDEX + # (both default to 0; override per-test to simulate custom .tmux.conf settings). + export MOCK_TMUX_BASE_INDEX="0" + export MOCK_TMUX_PANE_BASE_INDEX="0" function tmux() { local subcmd="${1:-}" shift || true @@ -205,7 +221,20 @@ setup() { done ;; show-options) - echo "0" + # Resolve which option was requested. Flags like -gv / -gwv + # precede the option name. + local opt="" + while [[ $# -gt 0 ]]; do + case "$1" in + -*) shift ;; + *) opt="$1"; shift ;; + esac + done + case "$opt" in + base-index) echo "$MOCK_TMUX_BASE_INDEX" ;; + pane-base-index) echo "$MOCK_TMUX_PANE_BASE_INDEX" ;; + *) echo "0" ;; + esac ;; esac return 0 @@ -441,7 +470,63 @@ assert_tmux_called_with() { } # ============================================================================== -# TEST 17: two invocations each create their own new-session call +# TEST 18: setup_tmux_session respects pane-base-index 1 (regression) +# ============================================================================== +# When a user's ~/.tmux.conf sets `setw -g pane-base-index 1` (very common in +# popular dotfiles / Oh My Zsh), tmux panes are numbered starting at 1, not 0. +# Previously Ralph hardcoded .0 / .1 / .2, so send-keys to .0 silently failed +# and the Ralph loop never started — leaving two empty panes and a stray +# tail -f. See: https://github.com/frankbria/ralph-claude-code/issues/ +@test "setup_tmux_session respects pane-base-index 1 for all pane targets" { + export MOCK_TMUX_PANE_BASE_INDEX="1" + + run setup_tmux_session + [ "$status" -eq 0 ] + + # With pane-base-index=1, the 3 panes are .1 (loop), .2 (output), .3 (status) + # Ralph loop command must target pane .1 (NOT .0 which doesn't exist) + assert_tmux_called_with "tmux send-keys -t .+\.1 .*(ralph|ralph_loop\.sh).*--live" + # live.log tail must target pane .2 (Claude Output) + assert_tmux_called_with "tmux send-keys -t .+\.2 tail -f" + # monitor must target pane .3 (Status) + assert_tmux_called_with "tmux send-keys -t .+\.3 .*(ralph-monitor|ralph_monitor\.sh)" + # No send-keys to .0 — that pane does not exist in this config + run grep -E '^tmux send-keys -t [^ ]+\.0 ' "$TMUX_CALL_LOG" + [ "$status" -ne 0 ] +} + +# ============================================================================== +# TEST 19: setup_tmux_session handles base-index 1 AND pane-base-index 1 +# ============================================================================== +# Both values non-zero is also common (users setting both together). Confirms +# the combination does not regress. +@test "setup_tmux_session respects both base-index and pane-base-index set to 1" { + export MOCK_TMUX_BASE_INDEX="1" + export MOCK_TMUX_PANE_BASE_INDEX="1" + + run setup_tmux_session + [ "$status" -eq 0 ] + + # Window 1 pane 1 = loop, 1.2 = output, 1.3 = status + assert_tmux_called_with "tmux send-keys -t [^ ]+:1\.1 .*(ralph|ralph_loop\.sh).*--live" + assert_tmux_called_with "tmux send-keys -t [^ ]+:1\.2 tail -f" + assert_tmux_called_with "tmux send-keys -t [^ ]+:1\.3 .*(ralph-monitor|ralph_monitor\.sh)" + assert_tmux_called_with "tmux rename-window -t [^ ]+:1 Ralph: Loop" +} + +# ============================================================================== +# TEST 20: get_tmux_pane_base_index returns 0 as default +# ============================================================================== + +@test "get_tmux_pane_base_index returns 0 as default" { + local result + result=$(get_tmux_pane_base_index) + [ "$result" -eq 0 ] + assert_tmux_called_with "tmux show-options.*pane-base-index" +} + +# ============================================================================== +# TEST 21: two concurrent setup_tmux_session invocations each create a tmux new-session call # ============================================================================== @test "two concurrent setup_tmux_session invocations each create a tmux new-session call" {