Skip to content

Wrapper gaps surfaced by libtmux-mcp adoption of 0.56.0 (14 sites) #670

@tony

Description

@tony

Summary

Adopting libtmux 0.56.0's new wrappers across tmux-python/libtmux-mcp (15 raw cmd() call sites → typed wrappers) surfaced 9 spots where the wrapper either doesn't exist or doesn't quite fit a tmux invocation that the underlying command supports. A follow-up sweep against tmux 3.6a's full cmd_entry flag schemas and format_table[] registry surfaced 4 more gap categories (gaps #10#13) plus an 8-token forward-looking set from tmux master post-3.6a (gap #14). tmux source citations below reference local copies at ~/study/c/tmux-3.6a/ (matching the upstream tmux/tmux@3.6a tag — the latest tmux release as of 2026-05-12) and link to the same files on GitHub at tmux/tmux/tree/3.6a.

Each gap is independently fixable; they cluster into five shapes (see end of issue).


1. Server.display_message doesn't exist

Site: Only Pane.display_message is wrapped (pane.py:625-774). Server has show_messages (message log) but no format-string evaluator.

Why tmux supports it: cmd-display-message.c:38-50 (local: ~/study/c/tmux-3.6a/cmd-display-message.c:38-50) — the entry's target is CMD_FIND_PANE with CMD_FIND_CANFAIL, so omitting -t falls back to the current/default pane and the format engine still resolves server-scoped variables like #{version} and #{socket_path}.

Where it bites callers: libtmux-mcp's get_server_info and _effective_socket_path need #{version} / #{socket_path} reads with no specific pane handle; they have to drop down to server.cmd("display-message", "-p", "#{…}"). Three sites in the MCP carry this workaround.

Proposed: add Server.display_message(format_string, *, get_text=True, target_client=…) returning list[str]. Same shape as Pane.display_message but no -t injection.


2. Window.display_message doesn't exist

Site: No wrapper at the Window level either.

Why tmux supports it: Same CMD_FIND_PANE | CMD_FIND_CANFAIL target as gap #1 (cmd-display-message.c:46); callers commonly pass -t @<window> for window-scoped reads like #{window_zoomed_flag}.

Where it bites callers: libtmux-mcp's resize_pane(zoom=…) needs the zoom state before deciding to toggle; with no Window.display_message, it must call window.cmd("display-message", "-p", "#{window_zoomed_flag}") directly.

Proposed: add Window.display_message (same signature as Pane.display_message), or generalize the Pane method to accept arbitrary targets.


3. Window.window_zoomed_flag is not a typed attribute on the dataclass

Site: window.py:53 — the Window dataclass declares many window_* fields but not window_zoomed_flag. mypy rejects window.window_zoomed_flag even after window.refresh().

Why tmux supports it: the variable is a first-class format token with a dedicated callback at format.c:2854-2864 (local: ~/study/c/tmux-3.6a/format.c:2854), formally registered in the format table at format.c:3557.

Where it bites callers: the umbrella issue libtmux-mcp#46 suggested swapping a display-message call for the ORM read — turned out it doesn't type-check, so the MCP reverted to cmd() with a comment.

Proposed: add window_zoomed_flag: str | None (and other common window_* format vars) as declared fields populated via refresh(). See gap #10 for the full undeclared token set across all object scopes.


4. Pane.reset() is still broken in 0.56.0 (open as #650)

Site: pane.py:2150-2153:

def reset(self) -> Pane:
    self.cmd("send-keys", r"-R \; clear-history")
    return self

The \; is tmux's interactive command separator. subprocess.run argv doesn't expand it — tmux sees a single argv "-R \; clear-history" and interprets the trailing \; clear-history as literal keys to send, never running clear-history. Pre-existing tracking issue: #650.

Why tmux supports both individually: send-keys -R is handled at cmd-send-keys.c:217-220 (local: ~/study/c/tmux-3.6a/cmd-send-keys.c:217); clear-history is its own entry in cmd-capture-pane.c.

Where it bites callers: libtmux-mcp's clear_pane (see tools/pane_tools/io.py) keeps a two-call workaround. Comment in the MCP cites this issue.

Proposed: split into two cmd() calls inside Pane.reset (the same workaround the MCP carries), or use tmux's \; separator via shell=True.


5. Pane.send_keys has no flag-only invocation

Site: pane.py:499-622. The wrapper always appends prefix + cmd (or copy_mode_cmd) as a trailing argv:

else:
    self.cmd("send-keys", *tmux_args, prefix + cmd)

There's no path that emits just tmux send-keys -R (reset flag, no key argument).

Why tmux supports it: cmd-send-keys.c:223-227 (local: ~/study/c/tmux-3.6a/cmd-send-keys.c:223) — when count == 0 and either -N or -R is set, the command returns CMD_RETURN_NORMAL without sending any keys. tmux's authors explicitly handle the flag-only case.

Where it bites callers: libtmux-mcp's clear_pane needs tmux send-keys -R (no keys). pane.send_keys("", reset=True, enter=False) produces tmux send-keys -R "" — likely a no-op in practice, but the trailing empty arg isn't what the underlying tmux command expects for a flag-only invocation. The MCP keeps pane.cmd("send-keys", "-R") for that reason.

Proposed: make the cmd parameter Optional[str] with default None; when None and a flag like reset or repeat is set, emit tmux send-keys <flags> with no trailing arg. Backward-compatible because cmd is currently positional-required.


6. Server.list_buffers() returns default-formatted lines, not raw names

Site: server.py:1643-1663 — calls tmux list-buffers with no -F, so the output is tmux's default template (buf_name: N bytes: …). No format= kwarg on the wrapper.

Why tmux supports it: cmd-list-buffers.c:35-44 (local: ~/study/c/tmux-3.6a/cmd-list-buffers.c:35) — entry usage is [-F format] [-f filter]; the -F template is read at line 56 with a builtin fallback, the -f filter at line 58.

Where it bites callers: libtmux-mcp's buffer GC needs just the names matching libtmux_mcp_* prefix. The wrapper returns lines like libtmux_mcp_abc: 5 bytes: "…"; parsing requires regex. The MCP keeps server.cmd("list-buffers", "-F", "#{buffer_name}") instead.

Proposed: add format: str | None = None and filter: str | None = None kwargs to Server.list_buffers. When format is set, pass -F <fmt>; otherwise keep the default tmux output for backward compat.


7. server.panes / QueryList doesn't expose tmux's -f C-filter

Site: [Server.panes at server.py:2060-2076](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/server.py#L2060) returns a QueryList[Pane] that filters in Python. Callers who want the **C-level filter** with format-expression predicates have no wrapper path; [fetch_objsin neo.py](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/neo.py#L248) always emits libtmux's own-F{format_string}` template.

Why tmux supports it: cmd-list-panes.c:37-42 (local: ~/study/c/tmux-3.6a/cmd-list-panes.c:37) — entry usage is [-as] [-F format] [-f filter]; the filter is read at line 125 and evaluated by format_true(expanded) at line 134, gating which panes appear in the output.

Where it bites callers: libtmux-mcp's search_panes fast path uses tmux list-panes -a -f '#{C/i:pattern}' -F '#{pane_id}' — pushes a regex match into tmux's C code, returning only matching pane IDs without capturing every pane in Python. With QueryList, the only option is "capture all panes in Python and filter post-hoc" — orders of magnitude slower for the common case. The MCP keeps server.cmd("list-panes", ...) for this site.

Proposed: add a filter: str | None = None kwarg to Server.panes (and the analogous Session.panes, Window.panes) that round-trips a tmux format expression through -f. Alternatively, a Server.list_panes(filter=…, format=…) low-level method.


8. LibTmuxException translation loses subcommand context

Site: wrappers like Session.last_window raise exc.LibTmuxException(proc.stderr) (session.py:298-318). The exception (exc.py:18-19 — a bare Exception subclass with no payload fields) carries the raw stderr list but not the subcommand name that produced it.

Why this matters: downstream wrappers (libtmux-mcp's handle_tool_errors translates LibTmuxException → ToolError(f"tmux error: {e}")) and any agent-facing error string lose the "which tmux command failed" context. Pre-0.56.0, the MCP built f"tmux {subcommand} failed: {stderr}" manually; the wrapper version surfaces only "tmux error: ['no last window']". One MCP test had to be updated to match the new shape.

Proposed: extend LibTmuxException (and friends) with an optional subcommand: str | None attribute. Format __str__ as f"{subcommand}: {stderr_text}" when present. Wrappers populate it implicitly (e.g., a _run_cmd("last-window", *args) helper that catches and re-raises with the subcommand).


9. Pane.cmd always injects -t <pane_id>, no warning on duplicate

Site: Pane.cmd at pane.py:178-212](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/pane.py#L178) always sets target = self.pane_idbefore delegating toServer.cmd. [Server.cmd at server.py:326 then emits -t <target> at the front of cmd_args regardless of what the caller passes in *args. If the caller also adds -t %X, tmux sees tmux <subcmd> -t %1 -t %1 and silently accepts the duplicate.

Why tmux tolerates it: tmux's args.c args_get() returns the last value of a repeated flag — so -t %1 -t %1 is functionally -t %1. Safe but masks caller bugs.

Where it bites callers: libtmux-mcp had two sites doing this (buffer_tools.py:275 and copy_mode.py:60-68) before the 0.56.0 wrappers were adopted; both were quietly accepted by tmux despite being incorrect Python. Discovered only because the new wrappers (which inject -t correctly) made the duplicate impossible to keep.

Proposed: when Pane.cmd (and Session.cmd, Window.cmd, Server.cmd) detect a -t already in *args, either (a) raise a clear TypeError, or (b) emit warnings.warn("Pane.cmd auto-injects -t; redundant -t in args") once per call site. Option (a) is safer; option (b) is gentler if the existing wild ecosystem relies on the no-op.


10. ~37 first-class format tokens aren't typed on Pane/Window/Session dataclasses

Site: neo.py:139-159 declares a hand-curated allowlist of format tokens as str | None = None fields on Obj. Sweeping tmux's format_table[] array at format.c:3010-3563 against neo.py's declared fields shows 37 scope-relevant tokens that ship in tmux 3.6a but are not declared. Gap #3 is one instance; the full set:

Pane (13) — registered in format.c between line 3254 and 3341:

Token format.c line
pane_dead 3254
pane_format 3269
pane_in_mode 3278
pane_input_off 3284
pane_key_mode 3287
pane_last 3290
pane_marked 3296
pane_marked_set 3299
pane_mode 3302
pane_path 3305
pane_pipe 3311
pane_synchronized 3326
pane_unseen_changes 3341

Window (13) — registered in format.c between line 3464 and 3557:

Token format.c line
window_active_clients_list 3464
window_active_sessions_list 3470
window_activity_flag 3476
window_bell_flag 3479
window_bigger 3482
window_end_flag 3491
window_flags 3494
window_format 3497
window_last_flag 3509
window_silence_flag 3542
window_start_flag 3548
window_visible_layout 3551
window_zoomed_flag 3557 — already in gap #3

Session (11) — registered in format.c between line 3359 and 3428:

Token format.c line
session_active 3359
session_activity_flag 3365
session_alert 3368
session_bell_flag 3380
session_format 3386
session_group_attached_list 3395
session_group_many_attached 3401
session_grouped 3407
session_many_attached 3416
session_marked 3419
session_silence_flag 3428

Why tmux supports them: every entry above appears verbatim in the format_table[] array — they're registered first-class format tokens with FORMAT_TABLE_STRING callbacks. tmux's format_create_defaults path populates them whenever the relevant scope is bound (pane→format_defaults_pane, window→format_defaults_winlink, session→format_defaults_session).

Where it bites callers: every undeclared token forces typed-Python callers back to pane.cmd("display-message", "-p", "#{…}"). libtmux-mcp's resize_pane(zoom=…) (window_zoomed_flag), copy-mode detection (pane_in_mode, pane_mode), and pane-death checks (pane_dead) all carry these workarounds.

Proposed: declare the full scope-relevant set on Obj (or split into per-scope mixins). Better: generate the field declarations from format.c parsing at build time so future tmux releases don't drift the typed surface — that closes gap #14 (HEAD tokens) for free.


11. No Client class — client_* format tokens have nowhere to land

Site: Server.list_clients at server.py:1392 returns raw output lines. No Client dataclass exists in src/libtmux/. Compare with Session/Window/Pane, which each have an Obj-backed dataclass and a QueryList-returning collection accessor.

Why tmux supports it: twelve client_* format tokens are registered in format.c:

Token format.c line
client_activity 3041
client_control_mode 3050
client_created 3053
client_last_session 3068
client_mode_format 3071
client_prefix 3080
client_readonly 3083
client_session 3086
client_termfeatures 3089
client_termtype 3095
client_theme 3098
client_utf8 3110

The matching tmux subcommand is cmd-list-clients.c, which accepts [-F format] [-t target-session] for arbitrary projection.

Where it bites callers: anything inspecting attached clients (multi-client coordination, "is this client read-only?", per-client status drawing) must call server.cmd("list-clients", "-F", "#{…}") and hand-parse output. The typed-ORM ergonomics that exist for sessions/windows/panes simply aren't available for clients.

Proposed: add src/libtmux/client.py with a Client(Obj) dataclass declaring all twelve client_* fields, plus Server.clients -> QueryList[Client] mirroring Server.sessions. Reuse fetch_objs with list_cmd="list-clients".


12. Server.run_shell drops -c (cwd) and -E (show_stderr) flags

Site: server.py:427-486 exposes background (-b), delay (-d), as_tmux_command (-C), and target_pane (-t). The full flag schema in tmux is "bd:Ct:Es:c:".

Why tmux supports them: cmd-run-shell.c:47 declares the args; cmd-run-shell.c:156-159 reads -c to override the working directory (cdata->cwd = xstrdup(args_get(args, 'c'))), and cmd-run-shell.c:161-162 reads -E to enable JOB_SHOWSTDERR — the latter is the difference between "stdout only" and "stdout + stderr merged into the captured output."

Where it bites callers: any caller that needs the shell command to run in a specific directory (independent of any target pane's cwd) or wants to surface stderr in the result must drop to server.cmd("run-shell", …). libtmux-mcp's diagnostic shell-outs (e.g., probing helper binaries) fall into this bucket.

Proposed: add cwd: StrPath | None = None and show_stderr: bool | None = None kwargs, emitting -c <cwd> and -E respectively.


13. Pane.capture_pane drops -P (pending input) flag

Site: pane.py:354-498 exposes 12 of the 13 flags from tmux's "ab:CeE:JMNpPqS:Tt:" schema. -P is the missing one.

Why tmux supports it: cmd-capture-pane.c:42 declares the args; cmd-capture-pane.c:231-232 branches: when -P is set, cmd_capture_pane_pending(args, wp, &len) returns bytes tmux has buffered as input for the pane but the program hasn't consumed yet — distinct from -p (lowercase, "print to stdout") and from the default history capture.

Where it bites callers: diagnostic tooling that needs to inspect "what input is queued but unprocessed?" (debugging hung programs, copy-mode race conditions, paste-buffer drains) has no typed access; callers must do pane.cmd("capture-pane", "-P", "-p").

Proposed: add pending: bool = False kwarg. When set, emit -P (and keep -p for stdout return). Document that this returns pending input bytes, not history.


14. Forward-looking: 8 new format tokens in tmux master post-3.6a

Site: None of these are declared on libtmux dataclasses (same shape as gap #10), and they don't yet exist in tmux 3.6a — they were added between the 3.6a tag and tmux master. Tracking them now means libtmux is ready when the next tmux tag lands.

Why tmux is adding them: present in tmux master (tmux/tmux/blob/master/format.c), absent from the 3.6a tag. The full diff:

Token Scope Purpose
pane_zoomed_flag pane Pane-level zoom indicator (companion to window_zoomed_flag)
pane_floating_flag pane Pane is a floating/popup pane
pane_flags pane Combined pane flag string (analogous to window_flags)
pane_pb_state pane Paste-buffer transfer state
pane_pb_progress pane Paste-buffer transfer progress
pane_pipe_pid pane PID of the pipe-pane child process
synchronized_output_flag terminal DEC mode 2026 synchronized-output state
bracket_paste_flag terminal Bracketed-paste mode active

Where it bites callers: none yet — these only become reachable when callers upgrade to the next tmux release. But the Obj allowlist will need to be re-walked when that happens. Treating gap #10's fix as "auto-generated from tmux's format.c" makes gap #14 free.

Proposed: roll into the same generator described under gap #10 — when tmux ships these in a tagged release, regenerate; no manual list to maintain.


Suggested groupings for fix PRs

These cluster into five shapes — each cluster is a coherent PR:

Cluster A — display_message parity (gaps #1, #2)
Add Server.display_message and Window.display_message with the same signature as Pane.display_message. Single-commit PR; mirrors the symmetry the rest of the 0.56.0 release set up.

Cluster B — typed format-token coverage (gaps #3, #10, #14)
Expand the declared field set on Pane/Window/Session to cover all scope-relevant tokens from format.c's format_table[]. Prefer a build-time generator that reads format.c so the typed surface tracks tmux automatically; that closes gap #14 (HEAD tokens) for free when the next tmux release tag lands.

Cluster C — flag-only / format-aware wrappers (gaps #5, #6, #7, #12, #13)
Make cmd optional on Pane.send_keys to enable flag-only invocations; add format= / filter= kwargs to Server.list_buffers and Server.panes (et al.) to expose tmux's C-level filtering; add cwd= / show_stderr= to Server.run_shell; add pending= to Pane.capture_pane. These all expose existing tmux capability that the wrapper currently hides.

Cluster D — new Client object hierarchy (gap #11)
Add Client(Obj) dataclass with the twelve client_* fields and Server.clients -> QueryList[Client]. Mirrors the Session/Window/Pane shape and brings list-clients into the typed API.

Cluster E — diagnostics and safety (gaps #4, #8, #9)
Fix Pane.reset per #650; thread subcommand context through LibTmuxException; add duplicate--t detection on the cmd methods. Quality-of-life cluster; each item is small but compounds across the wider ecosystem.


Evidence / Motivation

The MCP-side adoption PR is at tmux-python/libtmux-mcp#48. Gaps #1#9 are documented inline in commit messages or code comments there — pointers in the PR description and the umbrella issue libtmux-mcp#46. Gaps #10#14 surfaced from a follow-up sweep of tmux/tmux@3.6a's cmd_entry flag strings and format_table[] cross-referenced against src/libtmux/neo.py field declarations and src/libtmux/*.py wrapper signatures.

Metadata

Metadata

Assignees

No one assigned

    Labels

    MCPMCP server (src/libtmux/mcp/)enhancement

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions