Repository navigation
Wrapper gaps surfaced by libtmux-mcp adoption of 0.56.0 (14 sites) #670
Description
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 selfThe \; 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.
Activity
[-]Wrapper gaps surfaced by libtmux-mcp adoption of 0.56.0 (9 sites)[/-][+]Wrapper gaps surfaced by libtmux-mcp adoption of 0.56.0 (14 sites)[/+]docs(CHANGES): Drop duplicate Subcommand-tagged exceptions deliverable
docs(CHANGES,MIGRATION): Correct (#670) PR refs to (#672)
docs(CHANGES): Drop duplicate Subcommand-tagged exceptions deliverable
docs(CHANGES,MIGRATION): Correct (#670) PR refs to (#672)
docs(CHANGES): Drop duplicate Subcommand-tagged exceptions deliverable
docs(CHANGES,MIGRATION): Correct (#670) PR refs to (#672)
docs(CHANGES): Drop duplicate Subcommand-tagged exceptions deliverable
docs(CHANGES,MIGRATION): Correct (#670) PR refs to (#672)
docs(CHANGES): Drop duplicate Subcommand-tagged exceptions deliverable
docs(CHANGES,MIGRATION): Correct (#670) PR refs to (#672)
docs(CHANGES): Drop duplicate Subcommand-tagged exceptions deliverable
docs(CHANGES,MIGRATION): Correct (#670) PR refs to (#672)
docs(CHANGES): Drop duplicate Subcommand-tagged exceptions deliverable
docs(CHANGES,MIGRATION): Correct (#670) PR refs to (#672)
docs(CHANGES): Drop duplicate Subcommand-tagged exceptions deliverable
docs(CHANGES,MIGRATION): Correct (#670) PR refs to (#672)
feat(tmux) Client, typed fields, native filter (#672)