Skip to content

tmux parity: add interactive command coverage and align flag semantics with tmux#653

Open
tony wants to merge 105 commits intomasterfrom
tmux-parity
Open

tmux parity: add interactive command coverage and align flag semantics with tmux#653
tony wants to merge 105 commits intomasterfrom
tmux-parity

Conversation

@tony
Copy link
Copy Markdown
Member

@tony tony commented Mar 29, 2026

Summary

This PR expands libtmux's tmux command parity across Server, Session, Window, and Pane, with a focus on interactive and client-dependent commands. It also tightens several flag mappings and state-refresh behaviors so the high-level API matches tmux semantics more closely.

In addition to the new command coverage, this includes the three follow-up fixes from review:

  • Session.detach_client() no longer forces -s, so targeted detach behaves like tmux.
  • Window.move_window() now refreshes after successful moves, so returned objects are not stale after relative or cross-session moves.
  • ControlMode now binds client_name to the spawned client by matching client_pid, which fixes multi-client races.

Main Changes

New or expanded tmux command coverage

  • Add Server support for commands such as bind_key, unbind_key, list_keys, list_commands, start_server, lock_server, lock_client, refresh_client, suspend_client, server_access, run_shell, if_shell, source_file, buffer commands, confirm_before, command_prompt, and display_menu.
  • Add Session.detach_client() and window navigation helpers.
  • Add Window support for move_window flags, select_layout flags, last_pane, next_layout, previous_layout, rotate, swap, respawn, link, and unlink.
  • Expand Pane coverage for display_popup, capture_pane, send_keys, select, copy_mode, clock_mode, choose_*, customize_mode, display_panes, find_window, paste_buffer, clear_history, pipe, join, break_pane, move, respawn, and related flags.

tmux semantics and compatibility fixes

  • Correct several flag mappings and version gates to match tmux behavior.
  • Treat move-window -r as standalone renumbering.
  • Fix popup, prompt, paste-buffer, rotate, choose-tree, clear-history, capture-pane, and run-shell semantics where prior mappings diverged from tmux.
  • Add version annotations and parity docs where new parameters were introduced on existing APIs.

Test and tooling support

  • Add ControlMode and pytest fixture support for commands that require a real attached client.
  • Add extensive functional coverage for new command paths and regression cases.
  • Add parity-analysis support files under .claude/ and skills/tmux-parity/ to help maintain command coverage against tmux.

Testing

Ran:

  • uv run ruff check . --fix --show-fixes
  • uv run ruff format .
  • uv run mypy
  • uv run py.test --reruns 0 -vvv

Latest full run:

  • 1067 passed, 1 skipped

Notes

  • Diff vs origin/master is broad because this branch includes both the parity feature work and the review-driven correctness fixes on top.
  • The attached-client test infrastructure is intentionally part of this PR because several new tmux commands cannot be exercised correctly without a real client.

Comment thread src/libtmux/_internal/control_mode.py Fixed
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 29, 2026

Codecov Report

❌ Patch coverage is 48.59002% with 474 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.03%. Comparing base (e98cf4c) to head (1704a54).

Files with missing lines Patch % Lines
src/libtmux/pane.py 48.09% 109 Missing and 82 partials ⚠️
src/libtmux/server.py 48.00% 97 Missing and 72 partials ⚠️
src/libtmux/window.py 48.62% 32 Missing and 24 partials ⚠️
src/libtmux/_internal/control_mode.py 48.21% 28 Missing and 1 partial ⚠️
src/libtmux/session.py 52.08% 14 Missing and 9 partials ⚠️
src/libtmux/common.py 66.66% 2 Missing ⚠️
src/libtmux/options.py 50.00% 1 Missing and 1 partial ⚠️
src/libtmux/pytest_plugin.py 33.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #653      +/-   ##
==========================================
+ Coverage   46.58%   47.03%   +0.45%     
==========================================
  Files          22       23       +1     
  Lines        2372     3274     +902     
  Branches      390      700     +310     
==========================================
+ Hits         1105     1540     +435     
- Misses       1098     1376     +278     
- Partials      169      358     +189     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony
Copy link
Copy Markdown
Member Author

tony commented Mar 29, 2026

Code review

Found 4 issues:

  1. Pane.display_message(): no_expand maps to the wrong tmux flag. -I in display-message opens the pane for stdin input forwarding (window_pane_start_input()), not format suppression. The correct flag is -l (literal/no-expand, tmux 3.4+). Additionally, list_formats is also mapped to -l with the doc claiming it "lists format variables", but -l means suppress expansion; listing variables is done by -a (already mapped to all_formats). Both parameters are misassigned.

libtmux/src/libtmux/pane.py

Lines 700 to 714 in 857384a

tmux_args += ("-v",)
if no_expand:
tmux_args += ("-I",)
if notify:
tmux_args += ("-N",)
if list_formats:
if has_gte_version("3.4", tmux_bin=self.server.tmux_bin):
tmux_args += ("-l",)
else:
warnings.warn(
"list_formats requires tmux 3.4+, ignoring",
stacklevel=2,

  1. Window.swap() does not call self.refresh() after the operation, leaving window_index stale. The docstring example works around this by manually calling w1.refresh()/w2.refresh(), but the method itself never refreshes. Commit 3654a36e fixed the identical issue in move_window() for the same reason ("move-window can land on an index different from the requested target… leaving the returned Window stale"); swap-window has the same behaviour.

target_id = target.window_id if isinstance(target, Window) else target
tmux_args += ("-s", str(target_id))
proc = self.cmd("swap-window", *tmux_args)
if proc.stderr:
raise exc.LibTmuxException(proc.stderr)

  1. Server.show_prompt_history() and Server.clear_prompt_history() have no version guard. Both commands were added in tmux 3.3; libtmux's declared minimum is 3.2a. On tmux 3.2a the calls will raise LibTmuxException with a raw "unknown command" tmux error instead of a clean version message. The pattern established in the same PR for confirm_before and command_prompt (has_gte_version("3.3") + raise) should be applied here too.

libtmux/src/libtmux/server.py

Lines 1119 to 1158 in 857384a

def show_prompt_history(
self,
*,
prompt_type: str | None = None,
) -> list[str]:
"""Show prompt history via ``$ tmux show-prompt-history``.
Parameters
----------
prompt_type : str, optional
Prompt type to show (``-T`` flag). One of: ``command``,
``search``, ``target``, ``window-target``.
Returns
-------
list[str]
Prompt history lines.
Examples
--------
>>> result = server.show_prompt_history()
>>> isinstance(result, list)
True
"""
tmux_args: tuple[str, ...] = ()
if prompt_type is not None:
tmux_args += ("-T", prompt_type)
proc = self.cmd("show-prompt-history", *tmux_args)
if proc.stderr:
raise exc.LibTmuxException(proc.stderr)
return proc.stdout
def clear_prompt_history(
self,
*,

  1. display_popup docstring cross-reference uses the wrong module path: ~libtmux.test.control_mode.ControlMode — that module does not exist. The class lives at libtmux._internal.control_mode.ControlMode, which is already used correctly in server.py line 1013. This will produce a broken Sphinx link in generated docs.

libtmux/src/libtmux/pane.py

Lines 1202 to 1206 in 857384a

Requires tmux 3.2+ and an attached client. Use
:class:`~libtmux.test.control_mode.ControlMode` in tests to provide
a client.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony tony force-pushed the tmux-parity branch 2 times, most recently from bc12f79 to 64a9ad5 Compare May 2, 2026 18:16
tony added a commit that referenced this pull request May 2, 2026
…matrix

why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.

what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
  uses format_create_from_target without -T/-J, so a TTY-less CI
  server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
  introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
  not pipe run-shell stdout back through cmdq; restored upstream in
  3.5 by commit fb37d52d.
- Server.{command_prompt,confirm_before,show_prompt_history,
  clear_prompt_history} doctests: gate interactive demos behind
  has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
  test_show_prompt_history, test_clear_prompt_history on <3.3; skip
  test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
  a control-mode client via the existing fixture and pass
  target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
  split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
@tony tony marked this pull request as ready for review May 2, 2026 19:04
tony added 22 commits May 3, 2026 04:47
why: libtmux wraps ~28 of tmux's ~88 commands (~32% coverage). Need
tooling to systematically audit gaps, compare across tmux versions,
and guide implementation of new command wrappers.
what:
- Add .claude-plugin/ with manifest, commands, agent, skill, and scripts
- /parity-audit: generate full feature parity report (commands, flags,
  format variables, options)
- /version-diff: compare tmux features across 41 version worktrees
- /implement-command: guided workflow for wrapping new tmux commands
- parity-analyzer agent: auto-triggers on natural language parity queries
- tmux-parity skill: shared domain knowledge with reference files
  (command mapping, implementation patterns, C source navigation)
- Extraction scripts: parse tmux cmd-*.c and libtmux .cmd() invocations
…uto-discovery

why: Claude Code auto-discovers plugin components at the project root,
not inside .claude-plugin/. Agent wasn't showing up because it was
nested under .claude-plugin/agents/.
what:
- Move agents/, commands/, skills/ to project root
- Keep scripts/ in .claude-plugin/ (not auto-discovered)
- Remove custom path overrides from plugin.json
- Update cross-references between components
…d discovery

why: Claude Code discovers project commands from .claude/commands/ and agents
from .claude/agents/, not top-level directories.
what:
- Move 3 commands to .claude/commands/
- Move parity-analyzer agent to .claude/agents/
- Remove now-empty top-level commands/ and agents/ dirs
…ent, key-name flags

why: send-keys has many useful flags (reset terminal, hex input, repeat count,
format expansion, copy-mode commands) that were not exposed in the Python API.
what:
- Add reset (-R), copy_mode_cmd (-X), repeat (-N), expand_formats (-F),
  hex_keys (-H), target_client (-c, 3.4+), key_name (-K, 3.4+) parameters
- Version-gate target_client and key_name with has_gte_version("3.4")
- Add SendKeysCase NamedTuple parametrized tests for all new flags
… flags

why: select-pane has rich flag support for directional navigation, pane marking,
and input control that was not exposed in the Python API.
what:
- Add direction (-D/-U/-L/-R), last (-l), keep_zoom (-Z), mark (-m),
  clear_mark (-M), disable_input (-d), enable_input (-e) parameters
- Reuse existing ResizeAdjustmentDirection enum for direction flags
- Skip deprecated -P (style) and -g (show style) flags
- Add tests for direction, last pane, mark/clear, and input toggle
…, and style flags

why: display-message supports many useful flags for format queries and output
control that were not exposed in the Python API.
what:
- Add format_string (-F), all_formats (-a), verbose (-v), no_expand (-I),
  target_client (-c), delay (-d), notify (-N), list_formats (-l, 3.4+),
  no_style (-C, 3.6+) parameters
- Version-gate list_formats and no_style with has_gte_version
- Fix cmd argument handling: only pass when non-empty
- Add DisplayMessageCase NamedTuple parametrized tests
why: select-layout supports flags for spreading panes evenly and cycling
through layouts that were not exposed in the Python API.
what:
- Add spread (-E), next_layout (-n), previous_layout (-o) parameters
- Validate mutual exclusion between layout string and flag parameters
- Add tests for spread, next/previous cycling, and mutual exclusion
…number flags

why: move-window supports flags for positioning, conflict resolution, and
renumbering that were not exposed in the Python API.
what:
- Add after (-a), before (-b), no_select (-d), kill_target (-k),
  renumber (-r) parameters to move_window()
- Add tests for kill_target, renumber, and no_select behaviors
why: new-session supports flags for detaching other clients, suppressing
initial sizing, and specifying config files that were not exposed.
what:
- Add detach_others (-D), no_size (-X), config_file (-f) parameters
- Skip -A (attach-or-create) as it requires a terminal and does not work
  in libtmux's programmatic non-terminal context
- Add test for config_file parameter
why: new-window supports flags for replacing existing windows at a target
index and selecting existing windows by name that were not exposed.
what:
- Add kill_existing (-k) and select_existing (-S) parameters to new_window()
- -k destroys existing window at target index before creating new one
- -S selects existing window with matching name instead of creating new
- Add tests for both flags
why: split-window supports -p for percentage-based sizing which is more
intuitive than the absolute cell count provided by the existing size parameter.
what:
- Add percentage (-p) parameter to Pane.split(), mutually exclusive with size
- Validate that size and percentage are not both specified
- Add tests for percentage split and mutual exclusion
…kup flags

why: capture-pane supports flags for alternate screen capture, silent error
handling, and markup escaping that were not exposed.
what:
- Add alternate_screen (-a), quiet (-q), escape_markup (-M, 3.6+) parameters
- Version-gate escape_markup with has_gte_version("3.6")
- Add tests for quiet, alternate_screen, and escape_markup flags
…en to set_environment

why: show-options supports -q (quiet) and -v (values only) flags, and
set-environment supports -F (format expansion) and -h (hidden) flags that
were not exposed in the Python API.
what:
- Add quiet (-q) and values_only (-v) to _show_options_raw()
- Add expand_format (-F) and hidden (-h) to set_environment()
- Add tests for quiet show_options, hidden env vars, and format expansion
…story

why: clear-history is useful for clearing pane scrollback buffers,
especially important for test isolation and monitoring workflows.
what:
- Add clear_history() method with clear_pane (-H, 3.4+) parameter
- Version-gate -H flag with has_gte_version("3.4")
- Add test verifying history is cleared after sending commands
…window

why: swap-pane and swap-window are core layout manipulation commands needed
for programmatic pane and window reordering.
what:
- Add Pane.swap() with target, detach (-d), move_up (-U), move_down (-D),
  keep_zoom (-Z) parameters wrapping swap-pane
- Add Window.swap() with target, detach (-d) parameters wrapping swap-window
- Add tests verifying pane indices and window indices swap correctly
why: break-pane is essential for layout management, allowing a pane to be
moved into its own window programmatically.
what:
- Add break_pane() method with detach (-d) and window_name (-n) parameters
- Use -P -F#{window_id} to capture new window ID from output
- Return Window object via Window.from_window_id()
- Use server.cmd with explicit -s flag to avoid auto-target conflicts
- Add tests for basic break and named window
why: join-pane is the inverse of break-pane, needed for programmatically
merging panes between windows.
what:
- Add join() method with vertical (-v/-h), detach (-d), full_window (-f),
  size (-l), before (-b) parameters
- Accept Pane, Window, or string target ID
- Use server.cmd with explicit -s/-t to avoid auto-target conflicts
- Add roundtrip test (break + join) and horizontal join test
…respawn-window

why: respawn-pane and respawn-window are needed for restarting processes
in panes/windows without destroying and recreating them.
what:
- Add Pane.respawn() with shell, start_directory (-c), environment (-e),
  kill (-k) parameters wrapping respawn-pane
- Add Window.respawn() with same parameters wrapping respawn-window
- Add tests verifying respawn with kill on active panes and windows
why: pipe-pane is needed for monitoring, logging, and capturing pane output
to external commands or files programmatically.
what:
- Add pipe() method with command, output_only (-O), input_only (-I),
  toggle (-o) parameters wrapping pipe-pane
- Calling with no command stops piping
- Add test piping to file via cat, verifying output captured
why: run-shell executes shell commands in the tmux server context, useful
for background tasks and capturing command output programmatically.
what:
- Add Server.run_shell() with background (-b), delay (-d), capture (-C),
  target_pane (-t) parameters
- Returns stdout when not in background mode, None otherwise
- Add tests for basic command execution and background mode
…rotate

why: Window navigation commands (last/next/previous) and pane rotation are
commonly needed for programmatic window management workflows.
what:
- Add Session.last_window() wrapping last-window
- Add Session.next_window() wrapping next-window
- Add Session.previous_window() wrapping previous-window
- Add Window.rotate() wrapping rotate-window with direction_up (-U),
  keep_zoom (-Z) parameters
- Add tests for all navigation commands and rotation
why: link-window, unlink-window, and wait-for are needed for sharing windows
across sessions and for synchronization between tmux commands.
what:
- Add Window.link() wrapping link-window with target_session, target_index,
  kill_existing (-k), after (-a), before (-b), detach (-d) parameters
- Add Window.unlink() wrapping unlink-window with kill_if_last (-k) parameter
- Add Server.wait_for() wrapping wait-for with lock (-L), unlock (-U),
  set_flag (-S) parameters
- Add tests for link/unlink roundtrip and wait-for set_flag
tony added 21 commits May 3, 2026 04:47
…_formats

why: -I opens pane stdin input mode (window_pane_start_input), not suppress
     expansion. -l (tmux 3.4, commit 3be36952) is the correct literal flag.
     list_formats mapped to -l with doc "List format variables" — but -l
     suppresses expansion, not lists variables; -a (all_formats) already
     covers listing, making list_formats a wrong duplicate.
what:
- Fix no_expand to emit -l with has_gte_version("3.4") guard + warn on older
- Remove list_formats param from both overloads, implementation, and docstring
- Update no_expand docstring: "-l flag. Requires tmux 3.4+"
- Remove list_formats test case; add no_expand test verifying literal output
…matrix

why: PR #653 builds failed on tmux 3.2a-3.5 (3.6/master cancelled as
siblings). Investigation traced six categories: methods that need an
attached client, tests/doctests missing version guards, and a tmux
upstream regression in run-shell on 3.3a/3.4.

what:
- Server.show_messages: add target_client kwarg; cmd-show-messages.c
  uses format_create_from_target without -T/-J, so a TTY-less CI
  server raises 'no current client' unless -t <client> is supplied.
- Server.server_access: add 3.3+ version guard (server-access was
  introduced in tmux 3.3 per CHANGES FROM 3.2a TO 3.3).
- Server.run_shell doctest: drop stdout assertion. tmux 3.3a/3.4 do
  not pipe run-shell stdout back through cmdq; restored upstream in
  3.5 by commit fb37d52d.
- Server.{command_prompt,confirm_before,show_prompt_history,
  clear_prompt_history} doctests: gate interactive demos behind
  has_gte_version so the doctest is harmless on older tmux.
- tests/test_server.py: skip test_server_access_list,
  test_show_prompt_history, test_clear_prompt_history on <3.3; skip
  test_run_shell_basic on <3.5; rewrite test_show_messages to spawn
  a control-mode client via the existing fixture and pass
  target_client.
- tests/test_pane.py: skip test_split_percentage on <3.5 since
  split-window -p was broken in 3.4 (fixed per CHANGES 3.4 TO 3.5).
…coverage

why: codecov/project failed because the new wrappers' parameter
branches were untested (patch coverage 40.97%). Server.display_menu
had no test at all; Pane.display_popup had two ad-hoc tests covering
only a handful of flags.

what:
- tests/test_pane.py: replace test_display_popup_runs_command and
  test_display_popup_with_dimensions with a parametrized
  test_display_popup_flags driven by DisplayPopupCase, exercising
  basic, dimensions, position, start_directory, and the 3.3+ flags
  (title, border_lines, style, border_style, environment). Add
  test_display_popup_close_on_success for the -EE branch in
  isolation, test_display_popup_mutual_exclusion for the ValueError
  guard, and test_display_popup_close_existing for the -C branch.
- tests/test_server.py: add DisplayMenuCase + test_display_menu_flags
  covering basic, title, position, target_pane, starting_choice,
  and the 3.3+ flags (border_lines, style, border_style). Per
  Server.display_menu's own docstring the wrapper cannot run under
  ControlMode (tty.sy=0 makes menu_prepare return NULL and the call
  hangs); the test stubs server.cmd to capture and assert the
  constructed argv, the only deviation from the suite's
  "use real tmux" pattern. The deviation is documented in the test
  docstring per AGENTS.md guidance.

Patch coverage on server.py 62%→68%, pane.py 64%→70% (line-only;
branch coverage gains are larger since each parametrized case
exercises a distinct `if param is not None:` branch).
why: tmux's `detach-client -a` is server-wide (cmd-detach-client.c:92-101
loops the global client list with no session filter). The wrapper at
session.py:291 emitted `-a -t <client>` and was documented as
session-scoped, so callers asking to "keep target attached, detach
others" silently detached clients in other sessions too.

what:
- Session.detach_client(all_clients=True, target_client=...) now
  enumerates clients via `list-clients -t <session_id>` and issues
  one `detach-client -t <client>` per non-target client, instead of
  the broken `-a -t` form.
- Docstring expanded to document the upstream `-a` semantics and why
  the wrapper takes the manual route.
- Add test_detach_client_all_clients_session_scoped: opens a client in
  another session via control_mode and asserts it remains attached
  after the call.
why: tmux's display-message -C does not suppress style output. Per
the introducing commit (80eb460f, "Add display-message -C flag to
update pane while message is displayed", first in tmux 3.6) the
flag is passed to status_message_set as the no_freeze parameter
(see status.c:477 signature; cmd-display-message.c:155 call). It
allows the pane to keep updating while the message is shown.

The wrapper labelled `-C` as `no_style: bool` and documented it as
"Suppress style output", which is wrong. Callers asking for
"suppress styles" silently get the unrelated update-pane behaviour.

what:
- Rename Pane.display_message kwarg `no_style` → `update_pane` on
  the function and both @Overloads.
- Rewrite the docstring to describe the actual behaviour and cite
  upstream commit 80eb460f.
- Update the deferred warnings.warn message to match the new name.
why: pane.py and server.py inlined `from libtmux.common import
has_gte_version` and `import warnings` ~15 times across method
bodies, with no circular-import reason. Each repetition is slop
that obscures the module's dependencies and prevents ruff from
grouping imports. AGENTS.md "code should not declare what it needs
over and over" applies in spirit.

what:
- Hoist `import warnings` and `from libtmux.common import
  has_gte_version` to the top of pane.py.
- Hoist `from libtmux.common import has_gte_version` to the top of
  server.py (warnings was not used there).
- Drop the inline imports inside ~12 method bodies.
- Update tests/test_pane_capture_pane.py:test_capture_pane_trim_trailing_warning
  to monkeypatch `libtmux.pane.has_gte_version` instead of
  `libtmux.common.has_gte_version` — `from X import Y` binds Y in
  the importer's namespace, so once hoisted the pane module's
  binding is what the wrapper resolves at call time.
…gv check

why: the previous doctest only verified the bound method exists
(`>>> server.display_menu  # doctest: +ELLIPSIS` →
`<bound method ...>`), which is the exact anti-pattern AGENTS.md
forbids. End-to-end execution can't be doctested — once tmux
prepares a menu it returns CMD_RETURN_WAIT and the cmdq blocks
on user selection (cmd-display-menu.c:374), so the previous doc
incorrectly cited menu_prepare returning NULL.

what:
- Rewrite the docstring example to monkeypatch `server.cmd`,
  invoke `display_menu` with a representative flag set, and
  assert on the captured argv. Same approach as
  test_display_menu_flags in tests/test_server.py.
- Add `monkeypatch` to the doctest namespace via conftest.py so
  the example can use it. The fixture is already collected for
  every test; exposing it to doctests is one extra line.
- Update the prose to cite the correct CMD_RETURN_WAIT cause.
why: tmux's display-menu accepts -H selected-style (3.4+,
b770a429), -M always-mouse (3.5+, d8ddeec7), and -O stay-open (3.2+,
649e5970). The wrapper omitted all three, leaving callers without a
way to highlight the selected item, force mouse mode, or keep the
menu open after a release.

what:
- Add selected_style: str | None (-H), mouse: bool | None (-M),
  stay_open: bool | None (-O) parameters to Server.display_menu.
- selected_style and mouse are version-gated with warnings.warn on
  unsupported tmux; stay_open ships unconditionally (3.2+ is older
  than the project's minimum).
- Hoist `import warnings` into server.py top-level imports.
- Extend test_display_menu_flags with three new cases
  (with_stay_open, with_selected_style_v34, with_mouse_v35) and
  teach the assertion to skip bool values (which emit only the
  switch, not a value).
why: tmux's display-popup accepts three flags the wrapper omitted:
- -B (3.3+, 614611a8) opens the popup with no border at all and
  overrides -b border-lines unconditionally
  (cmd-display-menu.c:473-474, lines = BOX_LINES_NONE)
- -k (3.6+, 5c89d835) dismisses the popup on any keypress after the
  inner command exits (cmd-display-menu.c:501-505,
  POPUP_CLOSEANYKEY)
- -N (3.6+, 3c9e1013) clears all auto-close flags so the popup is
  not auto-dismissed (cmd-display-menu.c:490-491, flags = 0)

what:
- Add no_border (-B), close_on_any_key (-k), no_keys (-N) kwargs
  to Pane.display_popup with the right per-flag version guards.
- Reject no_border=True + border_lines=... with ValueError, mirroring
  the existing close_on_exit/close_on_success guard. tmux's -B
  overrides -b regardless, so the combination is meaningless.
- Extend test_display_popup_flags with no_border_v33,
  close_on_any_key_v36, no_keys_v36 cases.
- Add test_display_popup_no_border_with_border_lines_rejects.
why: tmux's command-prompt accepts three flags the wrapper omitted:
- -F (3.3+, 1bbdd2ab) passes the template through
  args_make_commands_prepare so format strings expand
- -l (3.6+, 2c08960f) disables splitting the prompt on commas —
  treat the whole prompt as a single literal
- -e (post-3.6, 1e5f93b7) closes the prompt when the user empties
  it via backspace (PROMPT_BSPACE_EXIT)

what:
- Add expand_format (-F), literal (-l), bspace_exit (-e) kwargs
  to Server.command_prompt with per-flag version guards.
- Add test_command_prompt_extra_flags using the monkeypatch argv
  pattern from test_display_menu_flags. End-to-end behaviour for
  these flags depends on tmux internals (format expansion, comma
  splitting, backspace exit) that are awkward to drive headless;
  the argv check confirms the wrapper emits the right flag.
why: tmux's copy-mode accepts two flags the wrapper omitted:
- -s source-pane (3.2+, c0602f35) lets a pane display another
  pane's history in copy mode — useful for scrolling/copying from
  one pane into an editor in another
- -d (3.5+, 4823acca) page-down on entry if already in copy mode

what:
- Add page_down (-d, version-gated to 3.5+) and source_pane (-s,
  unconditional since 3.2 is older than the project minimum) to
  Pane.copy_mode.
- Add test_copy_mode_source_pane (cross-pane history exercise) and
  test_copy_mode_page_down (3.5+ skipif).
why: tmux's choose-tree accepts a much larger surface than the
wrapper exposed (cmd-choose-tree.c:36, args="F:f:GK:NO:rstwyZ").
Real users need at least format/filter/sort to drive the chooser
programmatically, plus -Z to zoom while picking.

what:
- Add format_string (-F), filter_expression (-f), sort_order (-O),
  reverse (-r), zoom (-Z) kwargs to Pane.choose_tree.
- Use filter_expression rather than filter to avoid shadowing the
  Python builtin (ruff A002).
- Add test_choose_tree_with_flags exercising all five.
why: capture_pane hardcoded -p (pipe to caller's stdout) and offered
no way to send the capture into a named tmux buffer for later cross-
pane consumption — a real feature of cmd-capture-pane.c
(args="ab:CeE:JMNpPqS:Tt:", -b at line 256).

what:
- Add to_buffer: str | None kwarg. When set, the wrapper omits -p,
  emits -b <buffer> and returns None instead of stdout.
- Use @t.overload to keep the existing list[str] return type for the
  default path; only the to_buffer-set call returns None.
- Add test_capture_pane_to_buffer that captures into a named buffer
  and verifies the marker survived via show-buffer / delete-buffer.
why: tmux's show-messages takes -T (list terminal capabilities) and
-J (print job server summary) in addition to the message-log default
(cmd-show-messages.c:41 args="JTt:"). The wrapper docstring already
referenced both, but only -t was exposed. -T and -J are early-return
paths in cmd-show-messages.c that don't go through
format_create_from_target, so they work clientless — handy for
debugging tmux from a headless test run.

what:
- Add terminals (-T) and jobs (-J) bool kwargs to
  Server.show_messages.
- Update the docstring to note the clientless modes.
- Add test_show_messages_terminals_jobs which exercises both modes
  without spinning up a control_mode client.
…rite

why: tmux's server-access supports forcing a user to attach
read-only (-r) or allowing read-write attach (-w) — both implicitly
allow the user if not yet in the ACL (cmd-server-access.c:108-145).
The wrapper omitted both, leaving callers without the controls that
make this command useful.

what:
- Add read_only (-r) and write (-w) bool kwargs to
  Server.server_access. Mutually exclusive — tmux rejects -r -w with
  "cannot be used together", and the wrapper raises ValueError early
  to give a clearer Python-side trace.
- Add test_server_access_read_only_write_mutex for the guard and
  test_server_access_argv (monkeypatch capture) for the emitted
  flags. server-access's actual side effect requires real OS users
  and ACL state, so end-to-end testing is out of scope.
why: SKILL.md said "~28 of ~88 tmux commands wrapped" and listed
join-pane, swap-pane, run-shell, display-popup as high-priority
unwrapped — all of which the tmux-parity branch wraps.
command-mapping.md likewise listed last-pane, next-layout,
previous-layout, move-pane as alias/flag-covered, despite the branch
adding direct wrappers for each (commits dd8c65f, 2ab4c65,
aa00c45). Static numbers in generated docs go stale fast; static
"unwrapped" lists go stale faster.

what:
- Rewrite SKILL.md "Current Coverage Summary" to point at the
  extraction scripts rather than baking in a number/list that
  immediately rots. Note that effective coverage is 100%.
- Rewrite command-mapping.md "Covered by Alias/Flag" — there are
  now exactly four indirect cases (list-panes, list-windows,
  set-window-option, show-window-options), all reached through
  internal queries / option scoping. Updated count from 8/82 to 4/86.
- Convert ```bash command blocks to ```console with $ prefix per
  AGENTS.md.
…S.md

why: AGENTS.md "Shell Command Formatting" requires command examples
in docs to use ```console with a "$ " prefix (not ```bash) and to
keep one command per code block so the prompt is unambiguous and
copy-pastable. The plugin docs added in this branch had multi-command
```bash blocks scattered across .claude/commands and
skills/tmux-parity references.

what:
- .claude/agents/parity-analyzer.md: 3 blocks → console+$.
- .claude/commands/version-diff.md: 2 blocks → console+$.
- .claude/commands/implement-command.md: split the 6-command
  verification block into 6 console+$ blocks.
- skills/tmux-parity/references/tmux-command-table.md: 2 blocks →
  console+$ (one of which had two ls invocations — split per
  AGENTS.md "one command per block").
why: test_detach_client and test_detach_client_no_target_detaches_all_session_clients
covered the same path (no target_client → -s session_id scoping)
with identical fixture shape; the latter is strictly stronger
because it asserts on two attached clients rather than one. Keeping
both adds maintenance cost without adding signal.

what:
- Remove test_detach_client. The remaining tests (no_target_*,
  target_client, all_clients_session_scoped) cover the three real
  branches of Session.detach_client.
why: BufferCase already had an `append: bool | None` field but no
parametrisation case actually exercised it — the append behaviour
lived in a duplicative test_buffer_append function. The remaining
buffer tests (delete, save_load, save_append, list_buffers) test
genuinely different operations than set+show variations and stay
as their own test functions.

what:
- Add a `set_show_append` BufferCase that seeds the buffer with
  "first" and appends "_second" via append=True, asserting the
  concatenated content.
- Update test_buffer_set_show to interpret the existing `append`
  field by seeding the buffer and passing append=True.
- Remove the standalone test_buffer_append function.
why: test_capture_pane_quiet, test_capture_pane_alternate_screen,
and test_capture_pane_mode_screen each had the same "call with one
flag, assert isinstance(result, list)" shape. Three almost-identical
3-line functions are exactly the case parametrised tests are for.

The existing CAPTURE_PANE_CASES harness is intentionally focused on
output-content assertions (run a command, check pattern in output)
— folding flag smoke-tests into it would muddle that purpose. A
small dedicated parametrise is the right fit.

what:
- Replace the three smoke functions with one parametrised
  test_capture_pane_flag_smoke driving (kwargs, min_tmux_version)
  cases for quiet, alternate_screen, and mode_screen (3.6+).
why: a previous commit (3b83234) annotated new params on existing
methods but left the entirely-new methods unmarked. After the
parity branch, ~55 brand-new public wrappers ship without a
"versionadded" hint, so users can't tell from the docs which API
landed in 0.45 vs an older release. The Weave review's three
reviewers all flagged this (consensus: Suggestion → Important).

what:
- Insert `.. versionadded:: 0.45` into the docstring of every new
  public method on Server, Session, Window, and Pane.
- 27 methods on server.py, 5 on session.py, 7 on window.py, 16 on
  pane.py.
- Existing methods (capture_pane, display_message, Pane.select,
  Window.select, …) are left alone — they predate 0.45.
tony added 3 commits May 3, 2026 09:38
why: test_show_messages_terminals_jobs assumed -T/-J short-circuit
  before client lookup. That only holds on tmux >= 3.6, after
  upstream commit b52dcff7 ("Allow show-messages to work without a
  client") added CMD_CLIENT_CANFAIL to cmd_show_messages_entry. On
  3.2a/3.3a/3.4/3.5 the command queue rejects the call with
  "no current client" before cmd_show_messages_exec runs, so the
  clientless codepath is unreachable.
what:
- pytest.skip on tmux < 3.6 via has_gte_version("3.6")
- replace docstring rationale with the actual upstream cause
why: parity tests deliberately exercise version-specific tmux
  behaviour, so a failure on one tmux-version row should not cancel
  sibling jobs. With fail-fast on, a single failing version makes
  gh pr checks show all six rows as red and hides which versions
  actually pass.
what:
- set strategy.fail-fast: false on the build matrix
why: test_command_prompt_extra_flags[bspace_exit] failed on tmux
  3.2a because Server.command_prompt unconditionally passes -b,
  which requires tmux 3.3+ — the wrapper raises before the
  monkeypatched cmd is ever called. The bspace_exit parametrise
  tuple had min_tmux_version=None, treating "version needed for
  this flag" rather than "version needed for the wrapper".
what:
- set min_tmux_version="3.3" for the bspace_exit case
- comment why the gate is the wrapper-minimum, not the flag-minimum
@tony
Copy link
Copy Markdown
Member Author

tony commented May 3, 2026

Code review

Found 1 issue:

  1. Server.command_prompt(bspace_exit=True) emits -e with no version guard, while the sibling literal parameter on the same method does guard -l with has_gte_version("3.6") and a warnings.warn fallback. The docstring acknowledges that -e was added by upstream commit 1e5f93b7 on 2026-01-14 and is "not in any tagged release at the time of writing", so any caller passing bspace_exit=True on currently-released tmux (3.2a–3.6a) will get an unknown flag -e error from tmux. The CI hides this because tests/test_server.py::test_command_prompt_extra_flags[bspace_exit] monkeypatches server.cmd, never invoking real tmux for the assertion.

libtmux/src/libtmux/server.py

Lines 1066 to 1077 in 095758a

if literal:
if has_gte_version("3.6", tmux_bin=self.tmux_bin):
tmux_args += ("-l",)
else:
warnings.warn(
"literal requires tmux 3.6+, ignoring",
stacklevel=2,
)
if bspace_exit:
tmux_args += ("-e",)

Suggested fix: mirror the literal pattern — guard the -e append on has_gte_version (e.g. > 3.6 once a tag exists, or check tmux master) and emit a warning when the user requests bspace_exit=True on a version that lacks it.

Other items considered and dropped below the 80 confidence threshold: ControlMode _write_fd is not closed if subprocess.Popen raises before the registration loop (control_mode.py L61–L92) — real but error-path-only; and Session.detach_client(all_clients=True, target_client=...) enumerates list-clients then issues per-client detaches non-atomically — race window is microseconds and the docstring acknowledges it.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

tony added 3 commits May 3, 2026 11:33
why: command_prompt(bspace_exit=True) emitted -e unconditionally, but
  the flag was added by upstream tmux commit 1e5f93b7 on 2026-01-14
  and is not in any tagged release (verified: tag --contains returns
  empty; tmux 3.6a errors with "unknown flag -e"). The wrapper would
  break on every released version. Sibling literal flag (3.6+) shows
  the correct guard pattern.
what:
- bump TMUX_MAX_VERSION from "3.6" to "3.7" so master compares as
  "3.7-master" >= "3.7", letting bspace_exit work on master while
  still failing the gate on tagged releases 3.2a-3.6a
- guard the -e append with has_gte_version("3.7"); warn-and-ignore
  on older versions, matching the literal pattern at the same site
- update test_command_prompt_extra_flags[bspace_exit] gate to "3.7"
  so the case skips on every currently-released tmux
- bump test_version_parsing[next_version] fixture from "next-3.7" to
  "next-3.8" since "3.7" is no longer strictly greater than the new
  TMUX_MAX_VERSION
why: os.pipe() allocates both ends in the parent before subprocess
  spawn. The existing try/finally only closed read_fd; if Popen
  raised (ENOMEM, exec error, missing binary), self._write_fd
  stayed open. __exit__ won't run because __enter__ never returned,
  so the registration cleanup at the bottom of __enter__ is also
  unreachable. Net: one leaked fd per failed Popen.
what:
- wrap the existing try/finally (which closes read_fd) in an outer
  try/except that closes self._write_fd if anything propagates out
  of Popen, then re-raises
- use BaseException so KeyboardInterrupt during spawn also cleans up
why: test_allows_next_version asserts has_gt_version(TMUX_MAX_VERSION)
  for a mocked "tmux next-3.7" parse. Commit 3c883a8 bumped
  TMUX_MAX_VERSION to "3.7", so parsed "3.7" is no longer strictly
  greater than the max — assertion fails on every tmux job. Mirror
  the same fixture bump already applied to tests/test_common.py.
what:
- TMUX_NEXT_VERSION "3.7" -> "3.8" so the parsed version stays one
  minor ahead of TMUX_MAX_VERSION
@tony
Copy link
Copy Markdown
Member Author

tony commented May 3, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance against the 3 commits since the prior review (095758a..1704a54).

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

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.

2 participants