Skip to content

chore(deps[libtmux]): adopt libtmux 0.56.0 wrappers, drop raw cmd() escape hatches#48

Open
tony wants to merge 6 commits into
mainfrom
chore/adopt-libtmux-0.56-wrappers
Open

chore(deps[libtmux]): adopt libtmux 0.56.0 wrappers, drop raw cmd() escape hatches#48
tony wants to merge 6 commits into
mainfrom
chore/adopt-libtmux-0.56-wrappers

Conversation

@tony
Copy link
Copy Markdown
Member

@tony tony commented May 12, 2026

Closes #46.

Summary

libtmux 0.56.0 shipped ~50 new public methods that close the long-tail gap between the tmux(1) surface and the Python wrapper. The release notes call this out explicitly: "callers no longer need to drop down to Server.cmd(...) for the commands tmux ships out of the box."

This PR bumps the floor and adopts the wrappers across the eligible MCP call sites in five focused commits.

Commits

  1. py(deps[libtmux]): bump floor to 0.56.0 (was 0.55.1). Adds libtmux to [tool.uv.exclude-newer-package] so the 3-day cooldown does not block fresh releases for every contributor's uv sync — mirrors the existing gp-libs / gp-sphinx entries. CHANGES ### Dependencies entry.
  2. Pane(refactor[respawn]): adopt Pane.respawn from libtmux 0.56. Deletes the long stopgap comment block and the manual if result.stderr: branch.
  3. Pane(refactor): adopt copy_mode / pipe / clear_history wrappers. pane.cmd("copy-mode") → pane.copy_mode(), send-keys -X cmd cluster → pane.send_keys(copy_mode_cmd=…, repeat=…, enter=False), pipe-pane → pane.pipe, clear-history → pane.clear_history().
  4. Pane,Server(refactor[buffer]): adopt Pane.paste_buffer + Server.delete_buffer across buffer_tools.py, pane_tools/io.py, and server.py. Eliminates a duplicate -t pane_id (libtmux's Pane.cmd already injects it).
  5. Session(refactor): adopt Session.next_window / previous_window / last_window. Each returns the new active Window, raising on stderr — so the directional-navigation block collapses to a method-name lookup with no manual stderr handling or active_window re-fetch.
  6. Pane(refactor): adopt Pane.swap + Pane.display_message + Pane.select. One zoom-flag read in layout.py:74 stays on raw cmd() because window_zoomed_flag isn't a typed attribute on libtmux's Window in 0.56.0 and Window has no display_message wrapper yet — comment explains the gap.

Out of scope (no wrapper in 0.56.0)

These sites stay on raw cmd() per the umbrella issue:

  • _utils.py:159, server_tools.py:181/:251 — Server-scoped display-message reads (#{socket_path}, #{version}). Server has no display_message wrapper.
  • pane_tools/search.py:206list-panes -f #{C/i:…} C-filter expression; server.panes doesn't expose the -f shortcut.
  • server.py:256list-buffers -F #{buffer_name}. Server.list_buffers() returns tmux's default formatted lines, not raw names.
  • pane_tools/layout.py:74window.cmd("display-message", "-p", "#{window_zoomed_flag}"), see commit 6 note.

Test plan

Each commit was gated by the full verification chain:

  • rm -rf docs/_build
  • uv run ruff check . --fix --show-fixes — clean
  • uv run ruff format . — no changes
  • uv run mypy — Success: no issues found in 51 source files
  • uv run py.test --reruns 0 -vvv — 446 passed
  • just build-docs — build succeeded

One test was updated in commit 5: test_select_window_last_on_single_window_session_raises now matches "no last window" (tmux's actual stderr, surfaced via LibTmuxException → ToolError) instead of the old "last-window" subcommand-name substring that the previous manual error message contained.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 12, 2026

Codecov Report

❌ Patch coverage is 95.45455% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 84.87%. Comparing base (c02f4ae) to head (c0a7233).

Files with missing lines Patch % Lines
src/libtmux_mcp/tools/session_tools.py 75.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #48      +/-   ##
==========================================
- Coverage   84.91%   84.87%   -0.05%     
==========================================
  Files          40       40              
  Lines        2294     2268      -26     
  Branches      294      286       -8     
==========================================
- Hits         1948     1925      -23     
+ Misses        261      260       -1     
+ Partials       85       83       -2     

☔ 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 May 13, 2026

Code review

No issues found. Checked for bugs and AGENTS.md compliance.

🤖 Generated with Claude Code

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

@tony tony force-pushed the chore/adopt-libtmux-0.56-wrappers branch from 1e210dc to 53959f3 Compare May 13, 2026 00:55
tony added 6 commits May 12, 2026 20:47
why: libtmux 0.56.0 ships over fifty new public methods that close
     the long-tail gap between the tmux(1) surface and the Python
     wrapper. The MCP currently drops down to raw Server.cmd(...) for
     over a dozen commands that the new release now covers; bumping
     the floor unlocks the wrapper adoption in the follow-up commits
     on this branch.

what:
- pyproject.toml: libtmux>=0.55.1,<1.0 -> libtmux>=0.56.0,<1.0
- pyproject.toml: add libtmux to [tool.uv.exclude-newer-package] so
  the 3-day cooldown does not block fresh releases for every
  contributor's uv sync. Mirrors the gp-libs / gp-sphinx entries.
- uv.lock: libtmux 0.55.1 -> 0.56.0
- CHANGES: ### Dependencies entry naming the new wrappers this
  floor change unlocks (Pane.respawn, Pane.copy_mode, Pane.pipe,
  Pane.swap, Pane.paste_buffer, Pane.clear_history,
  Pane.display_message, Server.delete_buffer, Session navigation).
why: libtmux 0.56.0 ships Pane.respawn with the exact argv shape the
     stopgap predicted; the manual cmd("respawn-pane", *argv) call
     was a placeholder waiting on the release.

what:
- src/libtmux_mcp/tools/pane_tools/lifecycle.py: replace the manual
  argv builder + cmd("respawn-pane", *argv) + if result.stderr block
  with pane.respawn(kill=, start_directory=, environment=, shell=).
- Delete the multi-line stopgap comment that prescribed exactly this
  swap. Pane.respawn raises LibTmuxException directly, which the
  handle_tool_errors decorator already translates to ToolError.
why: libtmux 0.56.0 exposes typed wrappers for copy-mode entry,
     pipe-pane configuration, and clear-history. The MCP was hand-
     rolling argv tuples for all three; the wrappers replace that
     boilerplate while keeping behaviour identical.

what:
- tools/pane_tools/copy_mode.py: pane.cmd("copy-mode") -> pane.copy_mode().
  scroll-up + cancel send-keys -X invocations go through
  Pane.send_keys(copy_mode_cmd=..., repeat=..., enter=False) so the
  -X flag and -N repeat count are emitted by libtmux's argv builder.
- tools/pane_tools/pipe.py: pane.cmd("pipe-pane", ...) -> pane.pipe(...)
  for both the stop (no command) and start (cat redirect command) paths.
- tools/pane_tools/io.py:241: pane.cmd("clear-history") ->
  pane.clear_history(). The send-keys -R line above stays as a raw
  cmd() because Pane.send_keys would emit an extra empty key
  alongside the reset flag, and Pane.reset() is still broken upstream
  (libtmux #650).
why: libtmux 0.56.0 ships typed wrappers for paste-buffer and
     delete-buffer. The MCP was assembling argv lists by hand and
     redundantly injecting -t pane_id (libtmux's Pane.cmd already
     does that, leaving the duplicate behind as latent boilerplate).

what:
- tools/buffer_tools.py: paste_buffer tool now calls
  pane.paste_buffer(buffer_name=, bracket=) instead of building
  -b/-p/-t argv. The duplicate -t injection is gone.
- tools/pane_tools/io.py: paste_text's mid-flight paste uses
  pane.paste_buffer(buffer_name=, bracket=, delete_after=True);
  the cleanup leg uses server.delete_buffer(buffer_name=).
- server.py: _gc_mcp_buffers cleanup loop now calls
  server.delete_buffer(buffer_name=) directly. The list-buffers
  read keeps -F because Server.list_buffers() returns tmux's default
  formatted lines, not raw names.
why: libtmux 0.56.0 ships Session.next_window / previous_window /
     last_window wrappers that inject `-t $session_id`, return the
     new active Window, and raise LibTmuxException on stderr. The
     MCP's directional-navigation block was hand-rolling all three
     concerns.

what:
- tools/session_tools.py: collapse the next/previous/last-window
  cmd() dispatch into a method-name lookup. Each wrapper returns
  the new active Window, so the manual `if proc.stderr:` branch
  and the post-call `session.active_window` re-fetch are gone.
- tests/test_session_tools.py: the "no prior window" regression
  test now matches "no last window" (tmux's actual stderr surfaced
  via LibTmuxException -> ToolError) instead of the old "last-window"
  subcommand-name substring.
…46)

why: libtmux 0.56.0 ships typed wrappers for swap-pane and display-
     message that subsume the MCP's hand-built argv shapes. Pane.select
     was already available pre-0.56; this commit folds in its adoption
     alongside the new wrappers as part of the same layout/meta
     cleanup pass.

what:
- tools/pane_tools/layout.py:174: server.cmd("select-pane",
  target=target_pane.pane_id) -> target_pane.select().
- tools/pane_tools/layout.py:221: server.cmd("swap-pane", ...) ->
  source.swap(target=target) after resolving the target_pane_id to
  a Pane object.
- tools/pane_tools/meta.py:66: pane.cmd("display-message", "-p", fmt)
  -> pane.display_message(fmt, get_text=True). Same join logic over
  the returned list[str].
- tools/pane_tools/meta.py:162: same display_message wrapper applied
  to the snapshot_pane multi-format read.
- tools/pane_tools/layout.py:74: the window-zoomed-flag read stays
  on window.cmd("display-message", ...) — window_zoomed_flag isn't a
  declared attribute on libtmux's Window in 0.56.0 and Window has no
  display_message wrapper yet. Comment explains the gap so the next
  pass can pick it up when upstream lands either surface.
@tony tony force-pushed the chore/adopt-libtmux-0.56-wrappers branch from 53959f3 to c0a7233 Compare May 13, 2026 01:47
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.

Adopt libtmux 0.56.0 wrappers, drop raw cmd() escape hatches

2 participants