Skip to content

Type hints for tmux 3.4+ format tokens#674

Draft
tony wants to merge 65 commits into
masterfrom
typed-format-fields-tmux-3-4-plus
Draft

Type hints for tmux 3.4+ format tokens#674
tony wants to merge 65 commits into
masterfrom
typed-format-fields-tmux-3-4-plus

Conversation

@tony

@tony tony commented May 17, 2026

Copy link
Copy Markdown
Member

Summary

Adds typed Python attributes for format tokens tmux added in releases
after 3.2a — covering 3.4 (`pane_unseen_changes`), 3.5
(`pane_key_mode`), 3.6 (`session_active`, `session_activity_flag`,
`session_alert`, `session_bell_flag`, `session_silence_flag`,
`client_theme`), and the forward-looking set expected to land with
tmux 3.7+ (`bracket_paste_flag`, `pane_flags`, `pane_floating_flag`,
`pane_pb_progress`, `pane_pb_state`, `pane_pipe_pid`,
`pane_zoomed_flag`, `synchronized_output_flag`).

Problem solved

IDE autocomplete and `mypy` awareness for newer tmux format tokens.
Today users can read these via raw `display-message -p`, but the
typed surface saves the field-name lookup and centralizes the
version gate.

Why this is its own shipment

The forward-looking tokens can't be validated in CI until tmux 3.7
reaches a tagged release. Holding the shipment lets the typed
declarations land alongside real version-checked tests instead of
declarations gated by a version number that doesn't yet exist.

Status

Draft, gated on tmux 3.7 reaching a tagged release. This branch is
parented on `parity-pt-2` (#672); once #672 merges, rebase onto
`master` and revisit. The 3.4 / 3.5 / 3.6 tokens are already
validatable today and could land sooner if needed — but the
forward-looking set drives the held-back timing.

Acceptance criteria for merge

  • tmux 3.7 has reached a tagged release and is reachable from CI
  • Test matrix asserts `get_output_format(list_cmd, version)`
    returns the correct field set for every combination of
    `list_cmd ∈ {list-sessions, list-windows, list-panes, list-clients}`
    \× `version ∈ {3.2a, 3.3, 3.4, 3.5, 3.6a, 3.7}`
  • Negative tests prove master-only tokens are excluded on 3.2a
  • Integration test against tmux 3.7 in CI confirms forward-looking
    fields hydrate as expected

Test plan

  • `uv run ruff check . && uv run mypy src tests` — clean
  • `uv run pytest --reruns 0 -q` — 1265 passed, 2 skipped (the
    pre-existing 3.2a control-mode skips)
  • CI matrix green on `parity-pt-2` rows (3.2a through master)
    after the rebase target merges

Refs

tony added 30 commits May 16, 2026 08:57
why: tmux's display-message entry uses CMD_FIND_CANFAIL so -t is optional,
but libtmux only wrapped Pane.display_message. Server-scoped reads like
#{version} / #{socket_path} had to drop to server.cmd("display-message",
"-p", "#{...}") with no wrapper path. libtmux-mcp carries three workaround
sites.

what:
- Add Server.display_message mirroring Pane.display_message's signature minus
  -t injection (Server.cmd never auto-injects -t).
- Cover -p/-a/-v/-l/-N/-c/-d/-F flags; gate -l on tmux 3.4+.
- Doctests demonstrate #{version} and all_formats=True usage.
- Tests in tests/test_server.py use control_mode() so display-message has a
  client to dispatch -p output through (target/pane is unneeded but a client
  is needed for stdout to materialize).
why: Pane.display_message exists but Window doesn't, forcing callers like
libtmux-mcp's resize_pane(zoom=...) to drop down to
window.cmd("display-message", "-p", "#{window_zoomed_flag}") to read
window-scoped state.

what:
- Add Window.display_message mirroring Pane.display_message; Window.cmd
  auto-injects -t @<window-id>, so window-scoped reads (window_zoomed_flag,
  window_active_clients_list, …) work without a pane handle.
- Cover -p/-a/-v/-l/-N/-c/-d/-F flags; gate -l on tmux 3.4+.
- Doctests demonstrate #{window_id} and #{window_zoomed_flag} reads.
- Tests in tests/test_window.py via WindowDisplayMessageCase NamedTuple
  (matches the Pane.display_message test shape). Includes a target_client
  case using control_mode().
why: tmux's format.c registers window_zoomed_flag as a first-class format
token (callback at format.c:2854, table entry at format.c:3557), but the
libtmux Obj dataclass never declared it. mypy rejected
window.window_zoomed_flag even after refresh(). libtmux-mcp's
resize_pane(zoom) workflow worked around this by going through
display-message.

what:
- Add window_zoomed_flag: str | None = None to Obj in neo.py (alphabetically
  between window_width and wrap_flag).
- Auto-included in the tmux -F format string via get_output_format(), so
  refresh() populates it without further wiring.
- Test toggles zoom on/off via Pane.resize(zoom=True) across two refresh
  cycles and asserts "0"/"1" round-trip.
why: The previous body called self.cmd("send-keys", r"-R \; clear-history")
which sends a single argv to tmux via subprocess. tmux's \; is the
*interactive* command separator and is only interpreted when tmux re-lexes a
full command line — argv never gets re-parsed. tmux saw "-R \;
clear-history" as a single token after -R and treated "\; clear-history" as
literal keys to send, never executing clear-history. The scrollback was
never cleared.

what:
- Split reset() into two separate self.cmd("send-keys", "-R") and
  self.cmd("clear-history") calls. Each goes through Pane.cmd which auto-
  injects -t <pane-id>, so both target the right pane.
- Update docstring (uses r""" because of the literal \; explanation), with
  a working doctest that populates history and verifies reset.
- Test in tests/test_pane.py: spawn a shell pane, populate scrollback with
  "reset_marker_*" lines, call pane.reset(), assert the markers are gone
  from capture_pane(start=-100). Pre-fix this test would have failed (the
  markers stayed because clear-history never ran).
why: tmux's cmd-send-keys.c:223-225 deliberately handles count == 0 with -R
or -N set, returning CMD_RETURN_NORMAL without sending keys. The wrapper at
pane.py:619 always appended `prefix + cmd` to argv, so `pane.send_keys("",
reset=True, enter=False)` produced `tmux send-keys -R ""` — not the
flag-only path tmux explicitly supports. libtmux-mcp's clear_pane kept
`pane.cmd("send-keys", "-R")` for that reason.

what:
- Make cmd Optional[str] with default None. The previous positional-required
  signature is preserved for every existing caller (they pass cmd as a string).
- When cmd is None and copy_mode_cmd is None: emit `send-keys <flags>` with
  no trailing argv. Require at least one flag (reset, repeat, copy_mode_cmd);
  ValueError otherwise so degenerate `send_keys()` calls aren't silent no-ops.
- Skip the post-call self.enter() in flag-only mode (no keys → no Enter).
- Doctest demonstrates `pane.send_keys(reset=True)` working in flag-only mode.
- Tests use monkeypatch+stub of pane.cmd (the pattern from test_server.py:730)
  to capture the exact argv: flag-only reset emits `("send-keys", "-R")`,
  flag-only repeat=3 emits `("send-keys", "-R", "-N", "3")`. A separate test
  asserts ValueError when no flags accompany cmd=None.
why: tmux's cmd-list-buffers.c:39 declares `.args = { "F:f:", ... }`. The
libtmux wrapper passed neither, so callers got tmux's default template
`name: N bytes: "sample"` and had to regex-parse it. libtmux-mcp's buffer GC
carried `server.cmd("list-buffers", "-F", ...)` for that reason.

what:
- Add format_string and filter kwarg to Server.list_buffers. Default behavior
  (template output) preserved for backward compat.
- format_string follows the existing display_message convention (avoids
  shadowing Python's builtin `format`). filter shadows the builtin by design,
  with a per-line noqa: A002 + docstring note — it mirrors tmux's flag name
  for grep-friendly symmetry with the manual.
- Doctests cover all three modes (default, format projection, filter
  predicate); tests exercise raw-name projection and C-side filter matching
  (e.g. `#{m:gap6match_*,#{buffer_name}}` returns only the matching names).
…tmux C-side filter

why: tmux's cmd-list-panes.c:41 accepts `[-f filter]` and evaluates
`format_true(expanded)`, gating output server-side before any
data is returned. libtmux's `panes` / `windows` / `sessions` properties
return QueryList and force callers to filter post-hoc in Python — orders of
magnitude slower than pushing the predicate into tmux's C code.
libtmux-mcp's search_panes fast-path kept `server.cmd("list-panes", "-a",
"-f", ...)` for that reason.

Note: `list_panes()` / `list_windows()` / `list_sessions()` are already
defined as deprecated raise-only stubs (since 0.17) with pinned legacy-API
tests in `tests/legacy_api/`. Keep those intact and add the new methods
under `search_*()` — matches the verb libtmux-mcp uses for its consuming
endpoint and side-steps the legacy contract entirely.

what:
- Extend `fetch_objs` (neo.py:248) with `filter: str | None = None`. When
  set, append `-f <filter>` before the `-F` template. Single change feeds
  all the wrappers below.
- Add `Server.search_sessions`, `Server.search_windows`, `Server.search_panes`
  alongside the existing `sessions`/`windows`/`panes` properties.
- Add `Session.search_windows`, `Session.search_panes`.
- Add `Window.search_panes`.
- Each wrapper exposes a single `filter=` kwarg; the existing property is
  the no-filter form. Doctests demonstrate `#{m:gap7_*,#{window_name}}`-style
  predicates returning only matching objects.
- Tests across test_server.py / test_session.py / test_window.py exercise
  filter-by-id (m:pane_id) and filter-by-name (m:prefix_*).
…se_if_stderr helper

why: wrappers like session.last_window raise
exc.LibTmuxException(proc.stderr) and downstream consumers (libtmux-mcp's
handle_tool_errors) lose the "which tmux command failed" context. Pre-0.56
the MCP built `f"tmux {subcommand} failed: ..."` manually.

Split into two commits per planning direction:
* 8a (this commit): add the surface — LibTmuxException.subcommand attribute
  and raise_if_stderr helper. Backward-compatible; no call-site changes yet.
* 8b (next commit): mechanically migrate the ~12 existing raise sites to
  use raise_if_stderr.

what:
- LibTmuxException.__init__ accepts subcommand: str | None = None kwarg.
  Override __str__ to format as "<subcommand>: <stderr>" when set; otherwise
  preserves pre-0.57 output exactly. Verified backward-compat with a test
  that constructs exc with no kwarg and asserts no "subcommand:" prefix.
- common.raise_if_stderr(proc, subcommand) consolidates the
  `if proc.stderr: raise exc.LibTmuxException(...)` pattern. common.py
  already imports `exc`, so no new import. Documented with versionadded
  marker and a working doctest.
- Tests in tests/test_common.py cover both: the no-stderr no-op path
  (using session fixture for a started server) and the raises-with-tag path
  via list-clients against a fake session id.
why: mechanically thread the subcommand tag through every wrapper that
raises on tmux stderr. With 8a's surface in place
(LibTmuxException.subcommand + raise_if_stderr helper), this commit applies
the migration so every typed wrapper now produces an exception tagged with
the originating tmux subcommand.

what:
- Replace every `if proc.stderr: raise exc.LibTmuxException(proc.stderr)`
  pair with `raise_if_stderr(proc, "<subcommand>")` across the wrapper
  surface: server.py (sites), session.py (11), window.py (14),
  pane.py (22). Plus one explicit site in neo.py for fetch_objs's
  underlying tmux_cmd invocation.
- Migration was scripted with subcommand auto-extraction from the
  preceding `proc = …cmd("subcmd", …)` line; two unmapped sites
  (window.py's select_layout, neo.py's fetch_objs) migrated by hand.
- Add raise_if_stderr import to every touched module via ruff isort.
- New integration test in tests/test_session.py exercises the end-to-end
  tag: session.last_window() on a one-window session raises an exception
  with subcommand == "last-window" and str(exc) prefixed accordingly.
why: Server.cmd auto-injects -t <target> when the target= kwarg is set. A
caller's own positional -t produces `tmux <sub> -t %1 -t %1`; tmux's
args_get() applies last-wins so the positional -t is silently dropped. The
0.34 docstring at session.py:234 already documents this contract as ignored;
this commit promotes the documented contract into a runtime
DeprecationWarning so callers see the bug instead of getting silent no-ops.

what:
- Server.cmd: when target is not None and "-t" appears in *args, emit
  DeprecationWarning with stacklevel=3 so the warning surfaces at the
  caller (not the wrapper).
- Migrate tests/test_common.py:51 to use target= kwarg (the modern path).
- tests/legacy_api/test_common.py:204 now wraps the legacy call with
  pytest.warns(DeprecationWarning) to pin the new contract.
- New tests in tests/test_server.py exercise both the warning fires (legacy
  shape) and the no-warning case (target= alone).
- MIGRATION entry under "Upcoming Release" documents the deprecation, the
  migration path (target= kwarg), and the planned TypeError escalation.
why: tmux's format_table[] at format.c:3010-3563 registers 37 scope-relevant
format tokens that ship in 3.6a; libtmux's hand-curated allowlist in neo.py
declared only a subset. An earlier commit on this branch added window_zoomed_flag
specifically; this commit covers the remaining 36 so the typed dataclass
surface matches what tmux exposes.

what:
- src/libtmux/neo.py: add 13 pane_* (pane_dead, pane_format, pane_in_mode,
  pane_input_off, pane_key_mode, pane_last, pane_marked, pane_marked_set,
  pane_mode, pane_path, pane_pipe, pane_synchronized, pane_unseen_changes),
  12 window_* (window_active_clients_list, window_active_sessions_list,
  window_activity_flag, window_bell_flag, window_bigger, window_end_flag,
  window_flags, window_format, window_last_flag, window_silence_flag,
  window_start_flag, window_visible_layout), 11 session_* (session_active,
  session_activity_flag, session_alert, session_bell_flag, session_format,
  session_group_attached_list, session_group_many_attached, session_grouped,
  session_many_attached, session_marked, session_silence_flag) fields.
  Alphabetical insertion preserves existing layout. Each as
  `str | None = None`; get_output_format() auto-includes them in the tmux
  -F template.
- Tests: parametrized declaration + hydration tests per scope assert each
  field is registered on the dataclass and either None or a string after
  refresh(). On older tmux versions unknown tokens expand to empty strings,
  so older tmux still hydrates the rest of the fields fine.
- Focused live tests: pane.pane_synchronized round-trips through tmux's
  synchronize-panes window option; window.window_flags is always a string.
why: tmux's format.c at lines 3041-3110 registers twelve client_* format
tokens (client_activity, client_control_mode, client_created,
client_last_session, client_mode_format, client_prefix, client_readonly,
client_session, client_termfeatures, client_termtype, client_theme,
client_utf8) that the libtmux Obj dataclass didn't declare, and
Server.list_clients returned raw stderr-style strings instead of typed
objects. Multi-client coordination, read-only detection, theme/termtype
reads forced consumers down to server.cmd("list-clients", ...).

what:
- src/libtmux/neo.py: add the missing client_* fields to Obj
  alphabetically; extend ListCmd Literal with "list-clients".
- src/libtmux/client.py: new module with @dataclasses.dataclass class
  Client(Obj). refresh() uses obj_key="client_name", list_cmd="list-clients";
  classmethod from_client_name() mirrors the Session.from_session_id shape.
- src/libtmux/server.py: import Client; new Server.clients property returns
  QueryList[Client] via fetch_objs(list_cmd="list-clients").
- src/libtmux/__init__.py: export Client; add to __all__.
- conftest.py: register Client in the doctest_namespace.
- docs/api/libtmux.client.md: autodoc page.
- docs/api/index.md: card + toctree entry; updated lead to mention Client.
- tests/test_client.py: live tests using the control_mode() fixture exercise
  Server.clients listing, attached-session reporting, default readonly
  state, and refresh() rehydration.
why: tmux's cmd-run-shell.c at lines 156-162 reads two flags the wrapper
didn't expose: -c sets the shell command's working directory (independent of
any target pane's cwd) and -E sets JOB_SHOWSTDERR, which combines the
command's stderr into the captured output stream.

what:
- Server.run_shell gains `cwd: StrPath | None = None` (maps to -c) and
  `show_stderr: bool | None = None` (maps to -E). Both default-None, no
  behavior change for existing callers.
- Doctest demonstrates pwd in a custom cwd and stderr capture.
- Tests: `test_run_shell_cwd` runs `pwd` with `cwd=tmp_path` and asserts
  the directory appears in output; `test_run_shell_show_stderr` runs a
  shell snippet that writes to both streams and asserts both are in the
  result. Both gated by has_gte_version("3.5") because run-shell stdout
  passthrough requires tmux 3.5.
why: tmux's cmd-capture-pane.c at lines 231-232 branches on -P to call
cmd_capture_pane_pending, returning bytes tmux has buffered as input for the
pane but the program hasn't consumed yet. Useful for diagnosing hung
programs, copy-mode race conditions, and paste-buffer drains. The wrapper
covered 12 of 13 flags from "ab:CeE:JMNpPqS:Tt:" but skipped -P.

what:
- Pane.capture_pane gains `pending: bool = False` kwarg, present on both
  overload signatures and the implementation. When True, the wrapper
  appends -P alongside -p so stdout still flows back as a list.
- Docstring entry documents the distinction from the default capture
  (history vs unconsumed input).
- Tests: argv-assertion test confirms -P is emitted via the
  monkeypatch+stub pattern; a smoke test confirms the return type is
  list[str] (whether tmux has bytes to return depends on live input
  pressure and isn't reliably reproducible).
Document libtmux 0.57.0 per AGENTS.md changelog conventions: multi-sentence
lead paragraph, #### deliverables under ### What's new, the documented
Pane.reset fix under ### Fixes, the -t-in-args deprecation under ###
Deprecations, and the new Client autodoc page and migration entry under ###
Documentation. Every section describes ship-state user-visible behavior; the
bug history for stays scoped to ### Fixes where it is relevant to anyone
upgrading from 0.56.0.

Cross-links to autodoc'd APIs use {class}, {meth}, {attr}, {exc}, {func},
and {doc} roles so the changelog renders as live navigation in the docs
site.
why: the 0.34 versionchanged block on Session.cmd documented "Passing target
by -t is ignored. Use target keyword argument instead." Verified against
tmux source: tmux's args_get() returns TAILQ_LAST(...) — last value wins
(https://github.com/tmux/tmux/blob/3.6a/arguments.c). libtmux constructs
argv as ["-t", str(target), *args] (server.py:345), placing the kwarg's -t
value FIRST and any positional -t value SECOND. Under last-wins, the
*positional* value wins, not the kwarg. The original rationale was factually
inverted — readers would build wrong mental models of precedence.

what:
- Rewrite the .. versionchanged:: 0.34 block to describe the actual
  behavior. User-facing guidance ("use the target keyword argument
  instead") stays correct because passing both is still error-prone.
- Append a .. versionchanged:: 0.57 block noting the DeprecationWarning
  that Server.cmd now emits when both are set.
- No code change.

The same factual error appeared in the parallel Server.cmd versionchanged
0.57 block, fixed in the prior fixup commit. Window.cmd and Pane.cmd don't
carry the inverted note (verified).
why: CI matrix on tmux 3.2a continued to fail after the prior rollback with
`LibTmuxException: list-windows: ['server exited unexpectedly']`.
Cross-referencing libtmux's Obj.__dataclass_fields__ against
https://github.com/tmux/tmux/blob/3.2a/format.c showed eight tokens that
don't exist in tmux 3.2a's format_table[]:

  - client_theme
  - pane_key_mode
  - pane_unseen_changes
  - session_active
  - session_activity_flag
  - session_alert
  - session_bell_flag
  - session_silence_flag

tmux's format engine is documented as returning "" for unknown tokens
(format.c:4321 in 3.2a), but one or more of these specific tokens triggers a
server-side crash in 3.2a's format engine. Probably a NULL deref via the
options-lookup path (format_find calls options_parse_get first) when the
token name matches an option-style key that resolves to NULL in some
context.

The library's minimum supported tmux version is 3.2a per TMUX_MIN_VERSION in
src/libtmux/common.py, so a crash on that row blocks the matrix.

what:
- Drop the fields from Obj in src/libtmux/neo.py.
- Remove the corresponding entries from PANE_FORMAT_FIELDS in
  tests/test_pane.py and SESSION_FORMAT_FIELDS in tests/test_session.py.
- Keep the rest of the and token additions; the remaining
  ~tokens are all in 3.2a's format_table and don't cause crashes.

Follow-up: expose these fields via a version-gated mechanism (e.g. fetch the
full format string only for tmux versions that support the tokens, or split
Obj into core + augmented dataclasses) so users on 3.4+ / 3.6+ can still
read the tokens.

This is a forward commit (not autosquashed) so the rollback shows up clearly
in the history.
…a in list-windows

why: After the previous rollback that dropped tokens unknown to tmux 3.2a's
format_table, CI's 3.2a row still failed with `server exited unexpectedly`
during fetch_objs's `list-windows -F …` call. The remaining suspects were
the 11 client_* tokens added in the Client-class commit. tmux's format
engine evaluates the -F template against each window-link, with no client
bound (ft->c == NULL for the list-windows context). The callbacks check
ft->c != NULL and return NULL safely on 3.6a, but at least one of these
tokens triggers a server crash in 3.2a's format engine when invoked outside
its valid client scope.

Rather than chase the specific NULL-deref path through tmux 3.2a's C code,
drop these tokens from Obj. The Client dataclass keeps the 14 pre-existing
client_* fields (client_name, client_pid, client_termname, etc.) — enough to
populate Server.clients with attached-terminal identity, but no longer
covers the tokens added in this branch.

Dropped from Obj:
- client_activity, client_control_mode, client_created
- client_last_session, client_mode_format
- client_prefix, client_readonly, client_session
- client_termfeatures, client_termtype, client_utf8

what:
- src/libtmux/neo.py: remove the fields from Obj.
- src/libtmux/client.py: change the Client docstring doctest to read
  client_name (pre-existing) instead of client_readonly (removed).
- tests/test_client.py: remove test_client_session_reports_attached_session
  and test_client_readonly_default_zero (the fields they assert are gone).

Follow-up: re-expose these tokens via a scope-aware format string (query
client_* only when list-clients is the list_cmd, not when list-windows /
list-panes is). This is documented as a TODO and will land in a separate PR
once the safe-on-3.2a strategy is designed.

Forward commit (not autosquashed) so the rollback shows up clearly.
why: After dropping the new tokens that crashed tmux 3.2a's format engine,
four display_message tests now fail on the 3.2a CI row with empty stdout
from `display-message -p -c <control-mode-client>`:

tests/test_server.py::test_server_display_message_flags[version]
tests/test_server.py::test_server_display_message_flags[socket_path_format_string]
tests/test_server.py::test_server_display_message_target_client
tests/test_window.py::test_window_display_message_target_client

tmux 3.2a's display-message -p dispatch via a control-mode client does not
reliably deliver stdout back to the client process — the call succeeds (no
error) but the result list is empty. Later tmux versions (3.3+) fixed this
dispatch path.

The wrappers themselves work correctly on 3.2a — only the specific test
shape that asserts on stdout content via a control-mode client doesn't pass.
Skip these tests on tmux < 3.3 and keep them gated for the versions that
exercise the contract reliably.

what:
- tests/test_server.py: add `has_gte_version("3.3")` skip to
  test_server_display_message_flags (the parametrized cases that set
  target_client) and test_server_display_message_target_client.
- tests/test_window.py: same skip on test_window_display_message_target_client.
- Other display_message tests (the window_display_message_flags
  parametrize block that uses auto-injected -t and the
  no_text_returns_none tests) pass on 3.2a unchanged.

Forward commit (not autosquashed) so the version-gate decision shows up
clearly in the history.
why: the deprecation was reverted in the prior two commits because the rationale was factually inverted
(positional -t actually WINS via tmux's last-wins arg parsing — verified in
tmux's last-wins arg parsing in arguments.c via TAILQ_LAST). The
0.34 contract already requires target= for object-level cmd() overrides
(CHANGES:1075), so the 0.57 layer added misleading docstrings without new
user value.

what:
- Remove the "### Deprecations" section from the 0.57.0 entry.
- Remove the corresponding bullet under "### Documentation" pointing at
  the migration guide (the migration entry itself was removed when the
  Server.cmd warning commit was reverted).

The 0.34 contract stays in place. A future major release can re-evaluate
whether to enforce target=-only with a TypeError after a clean
pre-announcement.
why: 's CI matrix on tmux 3.2a crashed when the -F template included tokens
that don't apply to the calling list-* subcommand or don't exist in the
running tmux's format_table. The empirical crashers were 11 client_* tokens
queried during list-windows (no client context) and several post-3.2a tokens
that contributed cumulative risk.

what:
- src/libtmux/neo.py:
  - Add SCOPES_BY_LIST_CMD dict mapping each list-* to the set of token
    scopes its format engine can resolve (e.g. list-windows reaches
    universal + session + window; list-clients reaches universal +
    session + client).
  - Add FIELD_VERSION dict (initially empty) mapping field name → min
    tmux version; fields absent from the dict default to the project's
    floor (3.2a).
  - Add _SCOPE_PREFIXES table and _token_scope() helper that derive a
    token's scope from its name prefix (pane_*, window_*, session_*,
    client_*, buffer_*, etc.). Runtime-only tokens (mouse_*, cursor_*,
    selection_*, copy_cursor_*, popup_*) resolve to "event" and are
    excluded from all list-* templates.
  - Add _UNIVERSAL_TOKENS frozenset for cross-scope tokens without a
    scope prefix (pid, version, host, host_short, socket_path, etc.).
  - Add _normalize_tmux_version() helper that treats tmux master as a
    sentinel "newer than any tagged release" for comparison.
- Rewrite get_output_format() to take (list_cmd, tmux_version) and
  filter the field set accordingly. Cached via @functools.cache on the
  small number of (list_cmd, version) combinations a process sees.
- Rewrite parse_output() to take the same args so it reads the same
  filtered field order.
- Thread the live tmux version through fetch_objs() via
  get_version(server.tmux_bin) before calling get_output_format(),
  pass through to parse_output() per line.
- Doctests on the helpers and on get_output_format / parse_output
  demonstrate the new contracts.

No Obj field changes in this commit. The fields rolled back during the prior
CI bisect remain absent — they re-enter in follow-up commits that exercise
the new scope/version gating.
why: the scope-aware get_output_format introduced in the prior commit now
restricts each list-* subcommand's -F template to tokens whose scope is
reachable from that subcommand. The 11 client_* tokens (client_activity,
client_control_mode, client_created, client_last_session,
client_mode_format, client_prefix, client_readonly, client_session,
client_termfeatures, client_termtype, client_utf8) only appear when
fetch_objs is called with list_cmd="list-clients" — never during
list-windows, list-panes, or list-sessions. This eliminates the root cause
of the tmux 3.2a server crash that forced the original rollback.

what:
- src/libtmux/neo.py: re-add the 11 client_* fields on Obj alphabetically
  between the existing client_* declarations. No FIELD_VERSION entries
  are needed — all 11 ship in tmux 3.2a's format_table (verified against
  https://github.com/tmux/tmux/blob/3.2a/format.c).
- src/libtmux/client.py: restore the doctest reading client.client_readonly
  (a 0/1 string) to demonstrate the typed surface.
- tests/test_client.py: re-add test_client_session_reports_attached_session
  (asserts client.client_session matches the attached session name) and
  test_client_readonly_default_zero (asserts client.client_readonly is "0"
  for a normal attached client).

Verification: list-windows/list-panes/list-sessions stay scope-clean on tmux
3.2a — the format string for those subcommands contains no client_* tokens.
Confirmed via the runtime gate: tests pass on local tmux 3.6a; CI matrix run
will confirm 3.2a.
…cope prefix

why: the prefix-based scope router in _token_scope handles tokens whose
names start with pane_/window_/session_/client_/buffer_, but tmux's format
table contains tokens whose names don't carry a scope prefix (cursor_*,
mouse_*_flag, scroll_region_*, etc.). Without an override mechanism those
tokens either fall through to the wrong scope via a prefix rule or land in
the fail-closed "unknown" bucket and never appear in any list-* template.

Introduce _SCOPE_OVERRIDES as the targeted escape hatch: a per-token map
that wins before the prefix table. Subsequent scope-gate fix commits
populate it as misclassifications are audited against tmux's format_cb_*
callbacks.

what:
- Declare _SCOPE_OVERRIDES as an empty dict in src/libtmux/neo.py.
- _token_scope() consults it first, before _SCOPE_PREFIXES.
- Doctest in _token_scope() documents the override path without pinning
  a specific token (later commits add their own).
why: the 0.57.0 entry's "typed format-token fields" deliverable was
previously truncated to ~fields after the tmux 3.2a CI rollback. With
scope-aware + version-aware get_output_format in place, the full token set
re-enters the typed surface safely.

what:
- CHANGES: rewrite the deliverable section to describe scope+version
  gating (list-clients emits only client_* + universal; tokens added in
  tmux 3.4/3.5/3.6 are gated; 8 forward-looking master tokens are
  declared but hydrate only once tmux 3.7 ships). Cross-link each
  Pane/Window/Session/Client class.
why: Server/Window/Pane.display_message() called tmux then returned
proc.stdout without inspecting proc.stderr. On tmux errors (e.g. -F together
with positional cmd: "only one of -F or argument must be given") the wrapper
silently returned [] or None, hiding the failure from callers and
contradicting the rest of the typed-wrapper surface.

what:
- Insert raise_if_stderr(proc, "display-message") in all three
  display_message wrappers, matching the sibling display_popup /
  display_panes / display_menu wrappers in this branch.
- Add a negative test per scope verifying the wrapper raises
  LibTmuxException(subcommand="display-message") instead of swallowing
  tmux's "only one of -F or argument" error.
- Document the fix in CHANGES under Fixes.
why: The pending= docstring described tmux's -P as capturing "bytes tmux has
buffered as input for the pane but the running program hasn't consumed".
That's backwards: tmux is a terminal, it observes output from the running
program, not input to it. tmux's input_pending() returns ictx->since_ground,
an evbuffer that accumulates output bytes which begin an incomplete escape
sequence and are still pending the parser's ground state. Same wording
leaked into the CHANGES entry.

what:
- Rewrite the pending= parameter docstring in Pane.capture_pane() to
  describe parser-state semantics (incomplete escape sequences,
  since_ground), keeping the diagnostic-use mention.
- Mirror the same correction in the CHANGES entry for the kwarg.
why: The cwd= docstring framed the kwarg as a "working directory for the
shell command", parallel to subprocess.Popen(cwd=). tmux's -c is actually a
*start directory*: tmux/job.c:142 tries chdir(cwd), then chdir(home), then
chdir("/"), only fatal()'ing if all three fail. A user relying on Python
semantics expects a failed chdir to raise, but tmux silently falls back.
Reproduced live: run_shell('pwd', cwd='/definitely/not/a/path') returns
['<HOME>'].

what:
- Extend the cwd= parameter description to document tmux's home -> /
  fallback chain and contrast with subprocess.Popen(cwd=).
- Re-word the lead sentence "Working directory" to "Start directory"
  to match tmux's terminology.
…lient

why: After display_message() started propagating tmux stderr (the prior display_message-fix commit), tmux 3.2a's CI surfaced a latent issue in
test_server_display_message_no_text_returns_none: the control-mode client
path emits a usage error from tmux's argument parser on 3.2a, which the test
silently absorbed before but now raises. This is the same
control-mode-on-3.2a unreliability already gated on the sibling
test_server_display_message_flags and
test_server_display_message_target_client suites with
`has_gte_version("3.3")`.

what:
- Add the same `has_gte_version("3.3")` skip to
  test_server_display_message_no_text_returns_none, matching the
  existing gate on its sibling tests.
tony added 20 commits May 17, 2026 13:20
why: client.session_id / window_id / pane_id are hydrated from
tmux's downward format cascade at the moment the Client dataclass
is built and go stale as soon as the client switches view. The
existing class-level docstring warning isn't enough on its own —
users iterating over server.clients still reach for the raw fields
and treat them as identity.

what:
- Add Client.attached_session / .attached_window / .attached_pane.
  Each property re-reads list-clients before resolving and returns
  the live typed Session / Window / Pane (or None), mirroring the
  Session.active_window fresh-lookup convention.
- Tighten the Client class-level warning to point at the new
  properties as the safe accessors.
- Tests: typed resolution, fresh window tracking (selects a new
  active window post-hydration and asserts the property reflects
  it — proves the property re-queries rather than returning the
  snapshot), pane resolution, None propagation when session_id is
  absent.
- CHANGES: extend the Client what's-new entry to mention the
  attached_* accessors.
- MIGRATION: 0.57 section gains a "snapshots, not identity"
  subheading covering the snapshot vs. live access pattern.
why: The live attachment helpers promise None when a stored client no longer resolves through tmux list-clients.
what:
- Translate missing client refreshes to None for attached_session
- Cover real control-mode detach behavior for attached_* properties
why: The Client documentation should distinguish attached_* convenience behavior from explicit refresh lookups.
what:
- Clarify None behavior for missing live client rows
- Preserve refresh/from_client_name missing-object semantics
…e for 0.57.0 surface

why: The autodoc layer documents the new public symbols, but Client (view vs. identity), scope+version-gated typed fields, and C-side filter predicates each introduce a mental model that needs a topic-page home.
what:
- Add docs/topics/clients.md covering the Client view-vs-identity distinction, attached_session/window/pane live lookup, and None-on-detach semantics
- Add docs/topics/format-tokens.md explaining the two gates (scope and tmux version), the downward format_defaults cascade, the per-release compatibility table, and how to introspect via get_output_format
- Expand docs/topics/filtering.md with a (c-side-filtering)= section covering Python-side vs C-side trade-offs, predicate shapes, the silent-zero-match diagnostic recipe, and when to prefer which
- Add Client to docs/topics/architecture.md hierarchy diagram, table, core objects, and module map
- Add cross-links from docs/topics/traversal.md to c-side-filtering and from docs/topics/pane_interaction.md to capture_pane(pending=True) and send_keys(cmd=None)
- Wire the two new pages into docs/topics/index.md grid and toctree
- Rename autodoc anchor (clients)= to (api-clients)= in docs/api/libtmux.client.md so the conceptual page owns the readable {ref}\`clients\`
… docs

why: The 0.57.0 breaking-change docs in CHANGES and MIGRATION
promise str(exc) renders as "<subcommand>: <stderr>" and that
exc.args[0] holds the raw stderr string. raise_if_stderr passed
proc.stderr (a list[str]) directly to LibTmuxException, so
Exception.__str__ rendered the list's repr — yielding
"last-window: ['no last window']" instead of the documented
"last-window: no last window". The documented migration code
`exc.args[0] == "can't find window"` was always False because
exc.args[0] was the list, not the string.

what:
- Pass "\n".join(proc.stderr) to LibTmuxException so the message
  is a string. Multi-line tmux stderr renders as a multi-line
  string, matching how Python typically surfaces subprocess
  errors.
- Add test_raise_if_stderr_str_shape_exact that asserts the FULL
  str(exc) and exc.args shapes (no startswith, no substring) for
  a wrapper flowing through raise_if_stderr, so future drift
  surfaces as a test failure rather than a docs lie.
why: The two-call form raced under busy pane writers: send-keys -R
clears the visible grid (verified at ~/study/c/tmux/cmd-send-keys.c:225
→ input.c:923 → screen-write.c:335) and any output landing between
the two subprocess.Popen invocations could scroll into history via
scroll-on-clear (tmux's default), then be wiped by the second call's
clear-history. That destroyed output the caller produced after the
reset point — `reset()` should wipe state at reset time, not whatever
happens to be in the grid when clear-history runs.

A naïve one-call form `self.cmd("send-keys", "-R", ";",
"clear-history")` doesn't work either: Pane.cmd auto-injects
`-t <pane_id>` only before the first subcommand, so the `;` separator
leaves clear-history routed to tmux's cmdq default pane — empirically
verified on tmux 3.6a to clear the wrong pane.

what:
- Route through self.server.cmd to bypass Pane.cmd's auto-target, and
  pass `-t self.pane_id` explicitly on both subcommands so the `;`
  separator can't misroute clear-history.
- Update the docstring to describe the single-IPC semantics and why
  the explicit double-targeting is necessary.
- Add test_pane_reset_targets_non_active_pane that calls reset() on a
  non-active pane and asserts history_size goes to 0 on the target
  while the active sibling pane's history_size is preserved. Under
  the misroute bug, clear-history would have hit the active sibling
  instead of the target.
…rror

why: `Client` inherits `client_name: str | None` from `Obj`. The
`assert isinstance(self.client_name, str)` line vanishes under
`python -O`, letting `None` flow into `_refresh` and surfacing as
a less-clear downstream error. Keep the failure loud regardless of
optimization level.

what:
- Replace the assertion with an explicit `if self.client_name is
  None: raise ValueError(...)`, with the message documented in the
  Raises section of the docstring.
- Add test_client_refresh_raises_when_client_name_is_none asserting
  the explicit raise.
why: The fall-through `return "universal"` was fail-open — a future
`Obj` field added without a matching prefix, override, or known-token
entry would silently classify as universal and ship under every
`list-*` -F template. That defeats the scope-gating machinery on
exactly the case it's meant to protect: a future token whose class
hasn't been mapped, where emitting on tmux 3.2a may crash the
format engine or hydrate as nonsense.

Pre-flight confirmed: every currently-declared `Obj` field maps to
a known scope, so the flip is a no-op for the runtime template but
turns future drift into a deterministic test failure.

what:
- Change the final return in `_token_scope` from `"universal"` to
  `"unknown"`. `"unknown"` is absent from every SCOPES_BY_LIST_CMD
  entry, so an unclassified field is excluded from every list-cmd
  template.
- Document the fail-closed default in the docstring and show what
  it returns for an unrecognized name.
- Add test_token_scope_unknown_for_unclassified_field asserting the
  default and that `"unknown"` isn't in any allowed scope set.
- Add test_every_obj_field_classifies_to_known_scope as a guard:
  any new field added to Obj without classification breaks this
  test with a message pointing to the right table to update.
…s the triple

why: Code that needs all three of session/window/pane for a client
("where is this client attached *now*") naturally reads
client.attached_session, then client.attached_window, then
client.attached_pane. Each property re-reads tmux on access, so the
sequence costs three list-clients refreshes for one conceptual
read. The new helper shares a single refresh across the triple and
returns the three values together.

The helper catches NoActiveWindow and falls back to
(session, None, None). MultipleActiveWindows propagates — that
indicates a tmux invariant violation that callers should surface,
not absent attachment.

what:
- Add internal Client._resolve_attached returning
  tuple[Session | None, Window | None, Pane | None], with documented
  contract for the (None, None, None), (session, None, None), and
  full-triple cases.
- Update the class docstring to point readers at the helper for
  all-three access.
- Add three regression tests: live attachment → full triple,
  detach → (None, None, None), and a monkeypatch-driven
  NoActiveWindow → (session, None, None).
- attached_session / attached_window / attached_pane stay unchanged
  so per-access live semantics are preserved.
why: The 0.57.0 entry covered the LibTmuxException subcommand prefix
+ subcommand attribute under both `### Breaking changes` and
`### What's new` -> `#### Subcommand-tagged exceptions`. The
breaking-changes subsection already documents the behavior, migration
path, and rationale; the duplicate `####` heading restates the same
content without adding new information. Per CLAUDE.md's "deliverable
test," each `####` heading should be a distinct deliverable in user
vocabulary — this failed it.

what:
- Remove the `#### Subcommand-tagged exceptions (#670)` block from
  `### What's new`. The breaking-changes section at CHANGES:59-101
  is unchanged and remains the canonical reference for this
  deliverable.
why: MIGRATION's "Upcoming Release" header sat above an already-drafted
0.57.0 section without comment-bracket delimiters, so future-release
content didn't have an unambiguous insertion point. CHANGES uses
`<!-- KEEP THIS PLACEHOLDER -->` / `<!-- END PLACEHOLDER -->` to mark
where new entries land; MIGRATION should match for the same reason.

what:
- Wrap the placeholder body in matching HTML comment brackets,
  mirroring the CHANGES convention.
- New release content for the upcoming version lands below the END
  marker.
why: The doctest sent a key sequence then immediately read the pane's
scrollback to assert the marker landed. Without `retry_until`, this
trusted that the shell echoed before capture — a coin-flip under
parallel-test load. The dedicated functional test in
tests/test_pane.py::test_pane_reset_clears_history_and_sends_reset
already exercises the same path with retry_until; the doctest's
responsibility is to demonstrate the API, not to re-test timing.

what:
- Drop the send_keys + immediate capture_pane lines from the doctest.
- Keep the call + return-value check, which is timing-independent.
why: The filtering topic doc already covers when to pick `search_*()`
(C-side push-down) over `QueryList.filter()` (Python-side, post-fetch)
with a comparison table and "When to prefer which" guidance, but the
six `search_*` API entry points don't reference it. A caller landing
on `Server.search_panes` from autodoc has no path to discover the
comparison or the unfiltered `panes` attribute.

what:
- Add a See Also section to each `search_*` method (Server x3,
  Session x2, Window x1). Each block cross-links to (a) the matching
  unfiltered `panes` / `windows` / `sessions` attribute and (b) the
  `c-side-filtering` ref label in docs/topics/filtering.md.
…ic helper

why: The typed wrappers (`Server.search_*`, `Session.search_*`,
`Window.search_panes`, `Server.list_buffers`) all carry a warning
that tmux silently expands a malformed `-f` predicate to empty —
indistinguishable from "no matches". `fetch_objs` is the
documented public surface those wrappers route through, but its
docstring didn't carry the same caveat. A caller using
`fetch_objs(filter=...)` directly missed the warning.

what:
- Copy the malformed-filter warning into the `fetch_objs(filter=)`
  parameter docstring with the same wording as the typed wrappers.
- Cross-link to the `c-side-filtering` topic doc for the broader
  context.
why: Every 0.57.0 deliverable in CHANGES (and the MIGRATION header)
was tagged `(#670)`. Verified upstream: PR #670 does not exist on
tmux-python/libtmux. The actual branch PR is #672 ("Increase tmux
coverage: Client, typed fields, C-side filter"). The `(#670)` refs
appear to be from an earlier draft of the branch that never opened.

what:
- `sed 's/(#670)/(#672)/g'` across CHANGES and MIGRATION. Verified
  by `gh pr view` that #672 is open on the upstream and matches the
  branch's scope; #670 was a 404. The two pre-existing `(#672)`
  refs under `### Fixes` are unchanged.
…stead of raising

why: tmux's stderr from display-message conflates genuine argument-parser
errors (e.g. -F-with-positional rejection) with operational quirks like
3.2a's control-mode dispatch path silently failing without emitting stderr
at all. Raising LibTmuxException on every stderr forced an in-branch
workaround — pytest.skip patches gated on has_gte_version("3.3") — that
masked the underlying mismatch instead of solving it. Switch to
warnings.warn so callers see the stderr without losing the return value,
and the eventual raise/per-call-opt-in contract can land in a follow-up
shipment that exercises real tmux versions end-to-end.

what:
- src/libtmux/server.py, src/libtmux/window.py, src/libtmux/pane.py:
  replace raise_if_stderr(proc, "display-message") with
  warnings.warn("display-message: …", stacklevel=2). Wrapper return
  value unchanged on success and on warn paths.
- All three display_message docstrings gain a Notes block describing
  the warn-not-raise contract and showing the
  warnings.catch_warnings/filterwarnings("error") escalation pattern.
- tests/test_pane.py, tests/test_window.py, tests/test_server.py:
  rename test_*_display_message_raises_on_tmux_error to
  test_*_display_message_warns_on_tmux_error and switch to
  pytest.warns(UserWarning, match=…). Drop the 3.2a control-mode skip
  added by the prior commit on test_server_display_message_no_text_returns_none —
  with warn-not-raise the 3.2a control-mode stderr no longer fails the
  test (the test only asserts result is None on get_text=False).
- CHANGES: rewrite the display_message Fixes entry to describe the
  warn contract and how to escalate.
- MIGRATION: add a new section under 0.57.0 documenting the warn
  contract and the warnings.catch_warnings escalation pattern.
- MIGRATION: add a section noting that Pane.reset now dispatches via
  self.server.cmd; mocks targeting pane.cmd no longer intercept reset.
- docs/topics/pane_interaction.md: tighten the capture_pane(pending=True)
  wording to describe tmux's parser pending buffer rather than "slow
  consumer / paused program" (the latter framing implies a PTY/app
  buffering issue that pending= doesn't address).
- docs/topics/filtering.md: note that there is no search_clients();
  filter via Server.clients and Python-side QueryList.filter.
why: Server.new_session() called get_output_format() with no args, which
defaults to ("list-panes", "3.2a") and gates out every typed Obj field
whose FIELD_VERSION entry exceeds 3.2a. On tmux 3.3+ the returned Session
silently missed pane_dead_signal and pane_dead_time, and any 3.4+ tokens
that join FIELD_VERSION in a follow-up shipment would have the same
gratuitous gap. Match the pattern used by fetch_objs: thread the live
tmux version through, and use list-sessions as the scope (the format
context for tmux's new-session -P -F is the freshly created session).

what:
- src/libtmux/server.py:
  - Import get_version from libtmux.common.
  - new_session() now derives tmux_version via get_version(tmux_bin=…)
    and passes ("list-sessions", tmux_version) to both get_output_format
    (template build) and parse_output (output parse). Pair must be
    identical or the field order goes out of sync.

Verified: a new_session() on tmux 3.3+ now hydrates pane_dead_signal and
pane_dead_time on the returned Session, where master returned None
unconditionally.
why: tmux master (post-3.6a) registers eight new format tokens that the next
tmux release will ship: pane_zoomed_flag, pane_floating_flag, pane_flags,
pane_pb_state, pane_pb_progress, pane_pipe_pid, synchronized_output_flag,
bracket_paste_flag. Declaring them now means libtmux is ready when the tag
lands; older tmux releases expand unknown tokens to empty strings, so the
fields stay None until the user upgrades tmux.

what:
- src/libtmux/neo.py: add the fields alphabetically within the existing
  Obj layout (pane_* tokens among the pane_* block, bracket_paste_flag
  near buffer_*, synchronized_output_flag near start_time).
- tests/test_pane.py: parametrized test asserts each field is declared
  on the dataclass and hydrates either as None or as a string after
  refresh(). No runtime-value assertions — those will activate when the
  shipping tmux release exposes the tokens.
why: with the scope+version gating in place, the format string sent to older
tmux versions automatically excludes tokens that those versions don't
recognize. The tokens below first registered in tmux 3.4-3.6 — tagging them
with FIELD_VERSION makes them appear on supported tmux releases that include
them, and absent on older tmux without sending unknown tokens that bloat the
format string or trigger crashes.

what:
- src/libtmux/neo.py:
  - Re-add fields to Obj alphabetically: pane_key_mode,
    pane_unseen_changes, session_active, session_activity_flag,
    session_alert, session_bell_flag, session_silence_flag, client_theme.
  - Populate FIELD_VERSION with each token's minimum tmux release
    (3.4 for pane_unseen_changes, 3.5 for pane_key_mode, 3.6 for the
    five session_* tokens and client_theme).
- tests/test_pane.py: restore pane_key_mode and pane_unseen_changes in
  PANE_FORMAT_FIELDS (the parametrized declaration+hydration test).
- tests/test_session.py: restore the new session_* entries in
  SESSION_FORMAT_FIELDS.

Verification:
- On tmux 3.6a (local), all tokens hydrate via refresh(); tests
  pass.
- On tmux 3.2a, FIELD_VERSION skips all 8 — the -F template stays at
  its pre--rollback shape for that version.

Version anchors verified via: rg '"<token>"'
https://github.com/tmux/tmux/blob/<TAG>/format.c across 3.2a, 3.3a, 3.4,
3.5, 3.5a, 3.6, 3.6a.
why: tmux master post-3.6a registers new format tokens (verified via
https://github.com/tmux/tmux/blob/master/format.c grep on each format_cb_*
signature). Declaring them on Obj now means libtmux is ready when tmux 3.7
ships — the FIELD_VERSION gate keeps them silent on every currently-released
tmux, and they begin hydrating automatically once the user upgrades tmux.

what:
- src/libtmux/neo.py:
  - Add fields to Obj alphabetically: bracket_paste_flag, pane_flags,
    pane_floating_flag, pane_pb_progress, pane_pb_state, pane_pipe_pid,
    pane_zoomed_flag, synchronized_output_flag.
  - Tag each in FIELD_VERSION with "3.7" so they're absent in every
    list-* template on tmux <=3.6a.
  - Add _SCOPE_OVERRIDES dict for tokens whose name doesn't carry a
    scope prefix; map bracket_paste_flag and synchronized_output_flag
    to "pane" scope (their tmux callbacks dereference ft->wp). The six
    pane_* tokens scope correctly via the existing prefix table.
  - Update _token_scope() to consult _SCOPE_OVERRIDES first.

Verification: on tmux 3.6a (local), the fields stay None after refresh() —
confirmed because FIELD_VERSION blocks them from the -F template. All tests
pass.
@codecov

codecov Bot commented May 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.66120% with 133 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.84%. Comparing base (73f5740) to head (8db405d).
⚠️ Report is 128 commits behind head on master.

Files with missing lines Patch % Lines
src/libtmux/neo.py 33.65% 69 Missing ⚠️
src/libtmux/server.py 78.35% 17 Missing and 4 partials ⚠️
src/libtmux/client.py 60.41% 16 Missing and 3 partials ⚠️
src/libtmux/window.py 66.00% 12 Missing and 5 partials ⚠️
src/libtmux/session.py 83.33% 3 Missing ⚠️
src/libtmux/common.py 50.00% 2 Missing ⚠️
src/libtmux/exc.py 75.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #674      +/-   ##
==========================================
+ Coverage   47.11%   50.84%   +3.72%     
==========================================
  Files          23       25       +2     
  Lines        3296     3481     +185     
  Branches      709      682      -27     
==========================================
+ Hits         1553     1770     +217     
- Misses       1381     1416      +35     
+ Partials      362      295      -67     

☔ View full report in Codecov by Harness.
📢 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 tony force-pushed the parity-pt-2 branch 6 times, most recently from d886138 to 61688af Compare May 18, 2026 02:31
Base automatically changed from parity-pt-2 to master May 18, 2026 02:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant