From 3f907b6d38b8728eb1a5f76d9d7fe87ad0271697 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 12 May 2026 17:40:44 -0500 Subject: [PATCH 1/6] py(deps[libtmux]): bump floor to 0.56.0 (#46) 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). --- CHANGES | 4 ++++ pyproject.toml | 3 ++- uv.lock | 9 +++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 393d3e2..19b0e19 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,10 @@ _Notes on upcoming releases will be added here_ +### Dependencies + +**Minimum `libtmux>=0.56.0`** (was `>=0.55.1`). Unlocks the new tmux-command wrappers shipped in libtmux 0.56.0 — {meth}`~libtmux.Pane.respawn`, {meth}`~libtmux.Pane.copy_mode`, {meth}`~libtmux.Pane.pipe`, {meth}`~libtmux.Pane.swap`, {meth}`~libtmux.Pane.paste_buffer`, {meth}`~libtmux.Pane.clear_history`, {meth}`~libtmux.Pane.display_message`, {meth}`~libtmux.Server.delete_buffer`, and the {meth}`~libtmux.Session.next_window` / {meth}`~libtmux.Session.previous_window` / {meth}`~libtmux.Session.last_window` trio — so the MCP no longer falls back to raw `cmd()` calls for tmux commands the upstream API now covers. (#46) + ## libtmux-mcp 0.1.0a6 (2026-05-09) libtmux-mcp 0.1.0a6 is the activation and registration cleanup release. It makes the server much easier for MCP clients to discover from ordinary "pane", "window", and "session" prompts, standardizes new setup docs around the `tmux` registration slug, and adds migration guidance for existing `libtmux` registrations. Existing installs keep working; the release changes defaults and documentation so new installs line up with the tool prefix users actually see. diff --git a/pyproject.toml b/pyproject.toml index 15bd280..e1ab068 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ include = [ ] dependencies = [ - "libtmux>=0.55.1,<1.0", + "libtmux>=0.56.0,<1.0", "fastmcp>=3.2.4,<4.0.0", ] @@ -118,6 +118,7 @@ build-backend = "hatchling.build" # from any `exclude-newer` constraint (global or per-user) without # committing a date that would age into the lockfile. Mirrors the # pattern at vcspull/pyproject.toml for libvcs. +libtmux = false gp-libs = false gp-furo-theme = false gp-sphinx = false diff --git a/uv.lock b/uv.lock index 594d661..73031c6 100644 --- a/uv.lock +++ b/uv.lock @@ -12,6 +12,7 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P3D" [options.exclude-newer-package] +libtmux = false gp-sphinx = false sphinx-autodoc-sphinx = false sphinx-autodoc-api-style = false @@ -1119,11 +1120,11 @@ wheels = [ [[package]] name = "libtmux" -version = "0.55.1" +version = "0.56.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/01/fa4f839045d32576d4c193d85ba37e47908fea8227f18800847f589e2476/libtmux-0.55.1.tar.gz", hash = "sha256:dcee950537b8bda4337267bc2cb62b434c4c8da3a75c1546151674238ef14e20", size = 439372, upload-time = "2026-04-19T18:25:39.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/62/896e1e0412dd76c88926604d5a231feb9b116d6f32abe19054e244504dbc/libtmux-0.56.0.tar.gz", hash = "sha256:bddf52214405e4f64850826d44cbc958d4a01c53432983cee0e2856bdbbaaedb", size = 476168, upload-time = "2026-05-10T13:40:23.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/2e/819d7414b96f19ec4cafda95555246bdb9766dd7c0519b5b1bf4495789f7/libtmux-0.55.1-py3-none-any.whl", hash = "sha256:4382667d508610bdf71a7cc07d7a561d402fa2d5621cce299e7ae97b0cdcb93b", size = 80679, upload-time = "2026-04-19T18:25:37.61Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ce/4319c912164fa142956c73ba50ed6f2aac2ca7cced2e96c8320114f1c937/libtmux-0.56.0-py3-none-any.whl", hash = "sha256:ddf70de0f287666fb0f02082732f28eed46450de1828c995da3de2b12042ab60", size = 97768, upload-time = "2026-05-10T13:40:22.189Z" }, ] [[package]] @@ -1187,7 +1188,7 @@ testing = [ [package.metadata] requires-dist = [ { name = "fastmcp", specifier = ">=3.2.4,<4.0.0" }, - { name = "libtmux", specifier = ">=0.55.1,<1.0" }, + { name = "libtmux", specifier = ">=0.56.0,<1.0" }, ] [package.metadata.requires-dev] From ce425e48917b8c3b533c94092a24cc69639e04fd Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 12 May 2026 17:42:37 -0500 Subject: [PATCH 2/6] Pane(refactor[respawn]): adopt Pane.respawn from libtmux 0.56 (#46) 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. --- src/libtmux_mcp/tools/pane_tools/lifecycle.py | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/src/libtmux_mcp/tools/pane_tools/lifecycle.py b/src/libtmux_mcp/tools/pane_tools/lifecycle.py index ee82a16..4eb2e0a 100644 --- a/src/libtmux_mcp/tools/pane_tools/lifecycle.py +++ b/src/libtmux_mcp/tools/pane_tools/lifecycle.py @@ -150,30 +150,12 @@ def respawn_pane( "Use a manual tmux command if intended." ) raise ToolError(msg) - # Stopgap: ``libtmux>=0.55.1`` has no ``Pane.respawn()`` yet — the - # wrapper exists on the upstream ``tmux-parity`` branch (see - # ``libtmux/pane.py:respawn``) and mirrors this argv shape: ``-k``, - # ``-c ``, repeated ``-e=`` (single-arg form, NOT - # split ``-e KEY=VAL`` — tmux's args parser accepts both but - # upstream emits the joined form), then optional trailing shell. - # When the release line picks it up, swap ``pane.cmd("respawn-pane", - # *argv)`` for ``pane.respawn(kill=kill, start_directory= - # start_directory, environment=environment, shell=shell)`` and drop - # the stderr branch — ``Pane.respawn`` raises ``LibTmuxException``. - argv: list[str] = [] - if kill: - argv.append("-k") - if start_directory is not None: - argv.extend(["-c", start_directory]) - if environment: - argv.extend(f"-e{k}={v}" for k, v in environment.items()) - if shell is not None: - argv.append(shell) - result = pane.cmd("respawn-pane", *argv) - if result.stderr: - stderr = " ".join(result.stderr).strip() - msg = f"tmux respawn-pane failed: {stderr}" - raise ToolError(msg) + pane.respawn( + kill=kill, + start_directory=start_directory, + environment=environment, + shell=shell, + ) # Pick up fresh pane_pid and any command/path updates; tmux does # not invalidate the underlying object on respawn. pane.refresh() From 6060a230554a5398e978336d496b43f6b6a2a13c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 12 May 2026 17:44:29 -0500 Subject: [PATCH 3/6] Pane(refactor): adopt copy_mode / pipe / clear_history wrappers (#46) 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). --- src/libtmux_mcp/tools/pane_tools/copy_mode.py | 18 +++++++----------- src/libtmux_mcp/tools/pane_tools/io.py | 11 +++++++---- src/libtmux_mcp/tools/pane_tools/pipe.py | 4 ++-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/libtmux_mcp/tools/pane_tools/copy_mode.py b/src/libtmux_mcp/tools/pane_tools/copy_mode.py index ed776f6..6c9627a 100644 --- a/src/libtmux_mcp/tools/pane_tools/copy_mode.py +++ b/src/libtmux_mcp/tools/pane_tools/copy_mode.py @@ -55,16 +55,13 @@ def enter_copy_mode( session_id=session_id, window_id=window_id, ) - # libtmux's Pane.cmd injects ``-t pane.pane_id``; passing it again - # produced the duplicated ``-t %X -t %X`` shape tmux silently accepted. - pane.cmd("copy-mode") + pane.copy_mode() if scroll_up is not None and scroll_up > 0: - pane.cmd( - "send-keys", - "-X", - "-N", - str(scroll_up), - "scroll-up", + pane.send_keys( + "", + copy_mode_cmd="scroll-up", + repeat=scroll_up, + enter=False, ) pane.refresh() return _serialize_pane(pane) @@ -109,7 +106,6 @@ def exit_copy_mode( session_id=session_id, window_id=window_id, ) - # See enter_copy_mode: Pane.cmd injects ``-t pane.pane_id`` already. - pane.cmd("send-keys", "-X", "cancel") + pane.send_keys("", copy_mode_cmd="cancel", enter=False) pane.refresh() return _serialize_pane(pane) diff --git a/src/libtmux_mcp/tools/pane_tools/io.py b/src/libtmux_mcp/tools/pane_tools/io.py index 27e2cce..58af7f8 100644 --- a/src/libtmux_mcp/tools/pane_tools/io.py +++ b/src/libtmux_mcp/tools/pane_tools/io.py @@ -233,12 +233,15 @@ def clear_pane( session_id=session_id, window_id=window_id, ) - # Split into two cmd() calls — pane.reset() in libtmux <= 0.55.0 sends - # `send-keys -R \; clear-history` as one call, but subprocess doesn't - # interpret \; as a tmux command separator so clear-history never runs. + # Two separate calls — pane.reset() in libtmux 0.56.0 still sends + # `send-keys -R \; clear-history` as one call but subprocess doesn't + # interpret \; as a tmux command separator, so clear-history never + # runs. The bare `-R` send is left as a raw cmd() because + # Pane.send_keys requires a cmd string and would emit an extra + # empty key alongside the reset flag. # See: https://github.com/tmux-python/libtmux/issues/650 pane.cmd("send-keys", "-R") - pane.cmd("clear-history") + pane.clear_history() return f"Pane cleared: {pane.pane_id}" diff --git a/src/libtmux_mcp/tools/pane_tools/pipe.py b/src/libtmux_mcp/tools/pane_tools/pipe.py index 6f43a77..d443ac7 100644 --- a/src/libtmux_mcp/tools/pane_tools/pipe.py +++ b/src/libtmux_mcp/tools/pane_tools/pipe.py @@ -75,7 +75,7 @@ def pipe_pane( ) if output_path is None: - pane.cmd("pipe-pane") + pane.pipe() return f"Piping stopped for pane {pane.pane_id}" if not output_path.strip(): @@ -83,5 +83,5 @@ def pipe_pane( raise ToolError(msg) redirect = ">>" if append else ">" - pane.cmd("pipe-pane", f"cat {redirect} {shlex.quote(output_path)}") + pane.pipe(f"cat {redirect} {shlex.quote(output_path)}") return f"Piping pane {pane.pane_id} to {output_path}" From 0bae2dd089b48acaa2619f6ad3d5a3c945482986 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 12 May 2026 17:46:04 -0500 Subject: [PATCH 4/6] Pane,Server(refactor[buffer]): adopt paste_buffer + delete_buffer (#46) 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. --- src/libtmux_mcp/server.py | 2 +- src/libtmux_mcp/tools/buffer_tools.py | 6 +----- src/libtmux_mcp/tools/pane_tools/io.py | 13 +++++-------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/libtmux_mcp/server.py b/src/libtmux_mcp/server.py index bff7c8a..8bf3d2f 100644 --- a/src/libtmux_mcp/server.py +++ b/src/libtmux_mcp/server.py @@ -261,7 +261,7 @@ def _gc_mcp_buffers(cache: t.Mapping[_ServerCacheKey, Server]) -> None: if not name.startswith(_MCP_BUFFER_PREFIX): continue try: - server.cmd("delete-buffer", "-b", name) + server.delete_buffer(buffer_name=name) except Exception as err: logger.debug("buffer GC: delete-buffer %s failed: %s", name, err) diff --git a/src/libtmux_mcp/tools/buffer_tools.py b/src/libtmux_mcp/tools/buffer_tools.py index 28f9f41..e702c7f 100644 --- a/src/libtmux_mcp/tools/buffer_tools.py +++ b/src/libtmux_mcp/tools/buffer_tools.py @@ -268,11 +268,7 @@ def paste_buffer( session_id=session_id, window_id=window_id, ) - paste_args: list[str] = ["-b", cname] - if bracket: - paste_args.append("-p") - paste_args.extend(["-t", pane.pane_id or ""]) - pane.cmd("paste-buffer", *paste_args) + pane.paste_buffer(buffer_name=cname, bracket=bracket) return f"Buffer {cname!r} pasted to pane {pane.pane_id}" diff --git a/src/libtmux_mcp/tools/pane_tools/io.py b/src/libtmux_mcp/tools/pane_tools/io.py index 58af7f8..15870aa 100644 --- a/src/libtmux_mcp/tools/pane_tools/io.py +++ b/src/libtmux_mcp/tools/pane_tools/io.py @@ -328,13 +328,10 @@ def paste_text( msg = f"load-buffer failed: {stderr or e}" raise ToolError(msg) from e - # Paste from the named buffer. -d deletes only that named buffer, - # leaving any unnamed user buffer intact. - paste_args = ["-b", buffer_name, "-d"] - if bracket: - paste_args.append("-p") # bracketed paste mode - paste_args.extend(["-t", pane.pane_id or ""]) - pane.cmd("paste-buffer", *paste_args) + # Paste from the named buffer. ``delete_after=True`` (``-d``) + # deletes only that named buffer, leaving any unnamed user + # buffer intact. + pane.paste_buffer(buffer_name=buffer_name, bracket=bracket, delete_after=True) finally: if tmppath is not None: pathlib.Path(tmppath).unlink(missing_ok=True) @@ -342,6 +339,6 @@ def paste_text( # deletes it), but if paste-buffer failed before -d took effect # we leak an entry in the tmux server. Best-effort delete. with contextlib.suppress(Exception): - server.cmd("delete-buffer", "-b", buffer_name) + server.delete_buffer(buffer_name=buffer_name) return f"Text pasted to pane {pane.pane_id}" From fb4e467d613353ac76fa07bc261b1a2b21be5fc3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 12 May 2026 17:47:46 -0500 Subject: [PATCH 5/6] Session(refactor): adopt Session.{next,previous,last}_window (#46) 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. --- src/libtmux_mcp/tools/session_tools.py | 28 ++++++++++---------------- tests/test_session_tools.py | 12 +++++------ 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/libtmux_mcp/tools/session_tools.py b/src/libtmux_mcp/tools/session_tools.py index b2398e5..ca841ab 100644 --- a/src/libtmux_mcp/tools/session_tools.py +++ b/src/libtmux_mcp/tools/session_tools.py @@ -305,28 +305,22 @@ def select_window( window.select() return _serialize_window(window) - # Directional navigation: use the dedicated tmux subcommands so that - # libtmux's Session.cmd injects `-t $session_id` and the navigation - # stays scoped to this session (a bare `-t +` resolves against the - # attached client, not the target session). + # Directional navigation. Each Session method injects `-t + # $session_id`, returns the new active Window, and raises + # LibTmuxException on stderr — so the dispatch reduces to a + # straight lookup with no manual stderr handling. session = _resolve_session(server, session_name=session_name, session_id=session_id) - _CMD_MAP = { - "next": "next-window", - "previous": "previous-window", - "last": "last-window", + _NAV = { + "next": session.next_window, + "previous": session.previous_window, + "last": session.last_window, } assert direction is not None - subcommand = _CMD_MAP.get(direction) - if subcommand is None: + fn = _NAV.get(direction) + if fn is None: msg = f"Invalid direction: {direction!r}. Valid: next, previous, last" raise ToolError(msg) - proc = session.cmd(subcommand) - if proc.stderr: - stderr = " ".join(proc.stderr).strip() - msg = f"tmux {subcommand} failed: {stderr}" - raise ToolError(msg) - - active_window = session.active_window + active_window = fn() return _serialize_window(active_window) diff --git a/tests/test_session_tools.py b/tests/test_session_tools.py index 0fcc8af..fe3a284 100644 --- a/tests/test_session_tools.py +++ b/tests/test_session_tools.py @@ -271,15 +271,15 @@ def test_select_window_last_on_single_window_session_raises( ) -> None: """select_window last with no prior window must surface the tmux error. - Regression guard: session.cmd("last-window") on a session that has - never had a previously-active window emits "no last window" on - stderr, but the tool previously discarded the return value and - returned the unchanged active window as if the navigation had - worked. + Regression guard: ``Session.last_window`` on a session that has + never had a previously-active window raises ``LibTmuxException`` + with tmux's "no last window" stderr; the tool previously discarded + the return value and returned the unchanged active window as if + the navigation had worked. """ # The fixture session is freshly created: there is no previously- # active window for last-window to jump back to. - with pytest.raises(ToolError, match="last-window"): + with pytest.raises(ToolError, match="no last window"): select_window( direction="last", session_name=mcp_session.session_name, From c0a7233e05379bf79909621db2c02d67bcda3741 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 12 May 2026 17:50:28 -0500 Subject: [PATCH 6/6] Pane(refactor): adopt Pane.swap + Pane.display_message + Pane.select (#46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/libtmux_mcp/tools/pane_tools/layout.py | 9 ++++++--- src/libtmux_mcp/tools/pane_tools/meta.py | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libtmux_mcp/tools/pane_tools/layout.py b/src/libtmux_mcp/tools/pane_tools/layout.py index 40a689e..be4c74b 100644 --- a/src/libtmux_mcp/tools/pane_tools/layout.py +++ b/src/libtmux_mcp/tools/pane_tools/layout.py @@ -71,6 +71,9 @@ def resize_pane( ) if zoom is not None: window = pane.window + # ``window_zoomed_flag`` isn't declared on the libtmux Window + # dataclass in 0.56.0, and Window has no ``display_message`` + # wrapper — keep the raw cmd() until upstream surfaces one. result = window.cmd("display-message", "-p", "#{window_zoomed_flag}") is_zoomed = bool(result.stdout) and result.stdout[0] == "1" if zoom and not is_zoomed: @@ -171,7 +174,7 @@ def select_pane( idx = panes.index(active) step = 1 if direction == "next" else -1 target_pane = panes[(idx + step) % len(panes)] - server.cmd("select-pane", target=target_pane.pane_id) + target_pane.select() # Query the active pane ID directly from tmux to avoid stale cache target = window.window_id or "" @@ -216,8 +219,8 @@ def swap_pane( server = _get_server(socket_name=socket_name) # Validate both panes exist source = _resolve_pane(server, pane_id=source_pane_id) - _resolve_pane(server, pane_id=target_pane_id) + target = _resolve_pane(server, pane_id=target_pane_id) - server.cmd("swap-pane", "-s", source_pane_id, "-t", target_pane_id) + source.swap(target=target) source.refresh() return _serialize_pane(source) diff --git a/src/libtmux_mcp/tools/pane_tools/meta.py b/src/libtmux_mcp/tools/pane_tools/meta.py index 8bde535..1ef8467 100644 --- a/src/libtmux_mcp/tools/pane_tools/meta.py +++ b/src/libtmux_mcp/tools/pane_tools/meta.py @@ -63,8 +63,8 @@ def display_message( session_id=session_id, window_id=window_id, ) - result = pane.cmd("display-message", "-p", format_string) - return "\n".join(result.stdout) if result.stdout else "" + stdout = pane.display_message(format_string, get_text=True) + return "\n".join(stdout) if stdout else "" @handle_tool_errors @@ -159,8 +159,8 @@ def snapshot_pane( "#{pane_tty}", ] fmt = _SEP.join(_FMT_VARS) - result = pane.cmd("display-message", "-p", fmt) - raw = result.stdout[0] if result.stdout else "" + stdout = pane.display_message(fmt, get_text=True) + raw = stdout[0] if stdout else "" # Pad defensively to guarantee one slot per format var even if tmux # drops an unknown variable on older versions. parts = (raw.split(_SEP) + [""] * len(_FMT_VARS))[: len(_FMT_VARS)]