Skip to content

Commit d2f6ca0

Browse files
committed
fix(tools): Anchor select_pane direction=next/previous to the targeted window
why: window.cmd("select-pane", "-t", "+1") assembles to `tmux select-pane -t @window_id -t +1`. tmux's args_get() returns the LAST -t value (arguments.c:675-683), so the effective target is the bare `+1`. Bare relative pane targets resolve against `fs->current = c->session->curw` — the ATTACHED CLIENT's currently focused window (cmd-find.c:876-878, 513-517, 612-623), not any earlier -t on the command line. Practical effect: calling select_pane(direction="next", window_id=w2) while the client has w1 focused shifts the active pane in w1 and leaves w2 untouched. Reproduced on a scratch tmux server. This is the same class of bug that commit 8a8d7c6 fixed for select_window directional navigation, which I previously dismissed on select_pane based on a single-window probe that couldn't expose the misrouting. The fix uses the window-scoped relative pane spec `@window_id.+` / `@window_id.-`. The target-parser at cmd-find.c:1049-1097 splits on `.`, resolves the window part via window_find_by_id_str (cmd-find.c:317-321), and applies the offset against that explicit window's active pane (cmd-find.c:612-623 with fs->w already bound to the named window rather than s->curw). Routed through server.cmd(..., target=...) to bypass Window.cmd's auto-target injection that would otherwise reintroduce the duplicate-`-t` situation. what: - Switch the "next" and "previous" branches in select_pane from `window.cmd("select-pane", "-t", "+1"/"-1")` to `server.cmd("select-pane", target=f"{window.window_id}.+"/".-")`. - Add test_select_pane_next_previous_respects_target_window: a multi-window fixture where w1 is the active window and w2 is not. The test calls select_pane(direction="next", window_id=w2) and asserts (a) w2's active pane changed, (b) w1's active pane did NOT change, and (c) the returned PaneInfo describes a pane in w2. Verified to FAIL against the previous code and PASS with the fix. - No change to the "up/down/left/right/last" branches — those route through window.select_pane(flag), which uses the non- relative `-U/-D/-L/-R/-l` flags and is not affected by relative-target resolution.
1 parent 83e6105 commit d2f6ca0

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

src/libtmux_mcp/tools/pane_tools.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -870,9 +870,14 @@ def select_pane(
870870
if direction in _DIRECTION_FLAGS:
871871
window.select_pane(_DIRECTION_FLAGS[direction])
872872
elif direction == "next":
873-
window.cmd("select-pane", "-t", "+1")
873+
# Anchor the relative target to the requested window. A bare
874+
# `-t +` resolves against the attached client's current window
875+
# (tmux cmd-find.c), NOT the window we're targeting.
876+
# `@window_id.+` forces tmux to resolve the `+` offset against
877+
# the explicit window's active pane.
878+
server.cmd("select-pane", target=f"{window.window_id}.+")
874879
elif direction == "previous":
875-
window.cmd("select-pane", "-t", "-1")
880+
server.cmd("select-pane", target=f"{window.window_id}.-")
876881

877882
# Query the active pane ID directly from tmux to avoid stale cache
878883
target = window.window_id or ""

tests/test_pane_tools.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,65 @@ def test_select_pane_requires_target(mcp_server: Server) -> None:
696696
select_pane(socket_name=mcp_server.socket_name)
697697

698698

699+
def test_select_pane_next_previous_respects_target_window(
700+
mcp_server: Server, mcp_session: Session
701+
) -> None:
702+
"""select_pane direction=next/previous must anchor to window_id.
703+
704+
Regression guard: bare `-t +1` / `-t -1` pane targets resolve
705+
against the attached client's current window (tmux cmd-find.c),
706+
not against any earlier -t on the command line. Targeting a
707+
non-active window must use a window-scoped syntax like
708+
`@window_id.+` to actually affect that window. Without the fix,
709+
calling select_pane(direction='next', window_id=w2) when w1 is
710+
the client's active window shifts focus in w1 and leaves w2
711+
untouched.
712+
"""
713+
w1 = mcp_session.active_window
714+
assert w1.active_pane is not None
715+
w1.split()
716+
w1.split()
717+
w2 = mcp_session.new_window()
718+
w2.split()
719+
w2.split()
720+
721+
# Make w1 the active window again, so w2 is the NON-active target.
722+
w1.select()
723+
w1.refresh()
724+
w2.refresh()
725+
726+
w1_before = w1.active_pane.pane_id
727+
assert w2.active_pane is not None
728+
w2_before = w2.active_pane.pane_id
729+
730+
result = select_pane(
731+
direction="next",
732+
window_id=w2.window_id,
733+
socket_name=mcp_server.socket_name,
734+
)
735+
736+
w1.refresh()
737+
w2.refresh()
738+
assert w2.active_pane is not None
739+
w2_after = w2.active_pane.pane_id
740+
assert w1.active_pane is not None
741+
w1_after = w1.active_pane.pane_id
742+
743+
# Result must describe a pane in w2 (the target), not w1.
744+
w2_pane_ids = {p.pane_id for p in w2.panes}
745+
assert result.pane_id in w2_pane_ids, (
746+
f"select_pane returned {result.pane_id} which is not in target "
747+
f"window {w2.window_id}'s panes {w2_pane_ids}"
748+
)
749+
# w2's active pane must have actually changed.
750+
assert w2_after != w2_before, "target window w2's active pane did not change"
751+
# w1's active pane must NOT have changed — the wrong-window bug.
752+
assert w1_after == w1_before, (
753+
f"select_pane targeting w2 shifted focus in w1 "
754+
f"({w1_before} -> {w1_after}) — anchor missing"
755+
)
756+
757+
699758
# ---------------------------------------------------------------------------
700759
# swap_pane tests
701760
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)