Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
158 commits
Select commit Hold shift + click to select a range
d49669d
Ops(feat): Add typed operation and engine spine
tony Jun 21, 2026
4e87778
Ops(feat): Add classic + concrete engines and contract suite
tony Jun 21, 2026
7d3e0dd
Ops(feat): Add async engine, lazy plans, and op catalog
tony Jun 21, 2026
0dc6529
Ops(feat): Add persistent control-mode engine
tony Jun 21, 2026
e5252ed
Ops(feat): Add eager + lazy pane facades over the spine
tony Jun 21, 2026
0f1ee87
Ops(feat): Add async control-mode + concrete engines
tony Jun 21, 2026
2f5c565
Ops(feat): Add AckResult for no-output operations
tony Jun 21, 2026
732631c
Models(feat): Add pure object-graph snapshots
tony Jun 21, 2026
b53839f
Ops(feat): Add read-seam list operations
tony Jun 21, 2026
83380ab
docs(experimental): Add tmuxop-catalog directive
tony Jun 21, 2026
5703d68
ControlMode(fix): Consume startup ACK, drain unsolicited blocks
tony Jun 21, 2026
93e31c9
Ops(feat): Add lazy-plan chainability (>> and ; folding)
tony Jun 21, 2026
e990c10
Facade(feat): Add Server/Session/Window facades + creation ops
tony Jun 21, 2026
7e67fbd
Imsg(feat): Add native imsg engine + live parity test
tony Jun 21, 2026
5586d92
Facade(feat): Complete the facade matrix (Server/Session/Client)
tony Jun 21, 2026
bb77c83
chore(deps[dev]): Add ty type checker + config
tony Jun 21, 2026
7a444fc
Ops(feat): Add pluggable planners + {marked} fold
tony Jun 21, 2026
4f2eb99
Ops(feat): Add non-list read operations
tony Jun 21, 2026
794e4eb
Ops(feat): Add pane mutation/creation operations
tony Jun 21, 2026
1ce469a
Ops(feat): Add window mutation/navigation operations
tony Jun 21, 2026
8150abb
Ops(feat): Add server/option/environment operations
tony Jun 21, 2026
aba86c2
Ops(feat): Add paste-buffer operations
tony Jun 21, 2026
ff06424
docs(experimental): Document engines and lazy plans
tony Jun 21, 2026
decfec7
docs(CHANGES): Note experimental operations and engines
tony Jun 21, 2026
b13c6e5
Ops(fix): Correct move-window -k and paste-buffer -r
tony Jun 21, 2026
6463a2f
Ops(fix): Resolve SlotRef src_target in lazy plans
tony Jun 21, 2026
4e9a462
Ops(fix): Skip all decorates when a marked-fold create fails
tony Jun 21, 2026
3248b5d
Ops(fix): Mark save-buffer readonly to match its effects
tony Jun 21, 2026
1a725a2
Ops(docs): Fix PipePane parameter name in docstring
tony Jun 21, 2026
4374462
Ops(fix): Log imsg argv as a scalar tmux_cmd field
tony Jun 21, 2026
5c5abe4
Ops(docs): Add doctests to the planner plan() methods
tony Jun 21, 2026
e14089b
Ops(fix): Resolve decorate src_target in {marked} folds
tony Jun 21, 2026
39872d9
Ops(fix): Keep ; a bare separator in control-mode engines
tony Jun 21, 2026
1f3962c
Ops(fix): Treat a blank captured id as no id in marked folds
tony Jun 21, 2026
5746bf1
Ops(fix): Complete a marked fold whose creator does not capture
tony Jun 21, 2026
5fea2b8
Ops(fix): Drop create stdout when attributing marked decorates
tony Jun 21, 2026
cac2067
Ops(fix): Target the concrete pane in marked decorate results
tony Jun 21, 2026
07c5b92
Ops(fix): Decode SubprocessEngine output as UTF-8
tony Jun 21, 2026
f77d051
Models(refactor): Use namespaced dataclasses.replace in snapshots
tony Jun 21, 2026
2290b3f
Ops(docs): Fix PipePane -o flag description
tony Jun 21, 2026
3b299b4
Ops(fix): Mark save-buffer mutating (it writes a file)
tony Jun 21, 2026
2e0b267
Ops(fix): Correlate control-mode blocks per command and by flags
tony Jun 21, 2026
8b873db
Ops(fix): Clear pending futures on async control-mode write failure
tony Jun 21, 2026
ec30cdf
Ops(fix): Suppress ProcessLookupError on async cancel terminate
tony Jun 21, 2026
cbaab5f
Engines(fix): Remove the unreachable asyncio engine kind
tony Jun 21, 2026
cb512e7
Ops(fix): Normalize tmux master version in operation gates
tony Jun 21, 2026
de86e8c
Ops(fix): Reject SendKeys literal+enter combination
tony Jun 21, 2026
c8cf6e2
Ops(fix): Keep all lines of a display-message result
tony Jun 21, 2026
8065830
Ops(fix): Centralize the has-session stderr->stdout fold in the op
tony Jun 21, 2026
969201d
Engines(docs): Note ConcreteEngine query-simulation limits
tony Jun 22, 2026
334bd1b
Engines(fix): Avoid imsg UnboundLocalError on socket() failure
tony Jun 22, 2026
1675bf6
Engines(fix): Return imsg exit result on clean close after MSG_EXIT
tony Jun 22, 2026
27de17c
Engines(fix): Close imsg dup'd fds if the identify send never happens
tony Jun 22, 2026
e3c6414
Imsg(fix): Send identify LONGFLAGS frame once
tony Jun 22, 2026
88d56b8
Engines(test): Widen async control-mode coverage
tony Jun 22, 2026
2a9c37c
Ops(feat[send_keys]): Add suppress_history flag
tony Jun 22, 2026
0ad9c80
Ops(feat): Capture implicit child ids on create
tony Jun 22, 2026
57ef932
Workspace(feat): Declarative WorkspaceBuilder on the typed-ops Core
tony Jun 22, 2026
0cdd99f
Ops(feat): Serialize bindings + add plan preview
tony Jun 22, 2026
519bab7
Mcp(feat): Add framework-agnostic tool projection
tony Jun 23, 2026
e8a791e
Mcp(feat): Add optional fastmcp adapter (libtmux[mcp])
tony Jun 23, 2026
3417260
Mcp(feat): Per-op + plan tools and a stdio server
tony Jun 23, 2026
b4f47f6
Mcp(feat): Port mcp_swap config-swap dev script
tony Jun 23, 2026
ee4e27b
Tests(chore): Run the mcp adapter suite in the gate
tony Jun 23, 2026
190c82b
Workspace(test): Cover analyzer, compiler, runner
tony Jun 23, 2026
ad4575c
Mcp(feat): Add grok + agy CLIs to mcp_swap
tony Jun 23, 2026
fa09b96
Mcp(feat): Caller-aware async tmux tool surface
tony Jun 23, 2026
11a6789
Mcp(feat): Caller discovery + self-kill guards
tony Jun 23, 2026
5df8522
Mcp(fix): Harden self-kill guards + socket scoping
tony Jun 23, 2026
97f2ec2
Mcp(feat): Needle-free pane-output monitor
tony Jun 23, 2026
e3f040d
Mcp(feat): Make wait_for_output discoverable
tony Jun 24, 2026
40fdcc1
Mcp(fix): Close self-kill guard deferrals
tony Jun 24, 2026
3db193d
Mcp(fix): Harden wait_for_output monitor
tony Jun 24, 2026
2b8fd40
Workspace(feat): Thread env/shell/options through declarative tier
tony Jun 27, 2026
4b67d35
Workspace(feat): Add per-command Command (enter + sleeps)
tony Jun 27, 2026
7182ac9
Ops(feat): Add ForwardCaptureError + ShowOptionsResult.get_int
tony Jun 27, 2026
d1c811c
Workspace(feat): Add Workspace.to_dict + read suppress_history
tony Jun 27, 2026
96550b0
Workspace(feat): Add BuildEvent stream + on_event observer
tony Jun 27, 2026
636ee9c
Workspace(feat): Add opt-in wait_pane readiness (anti-race)
tony Jun 27, 2026
7782467
Workspace(feat): Honor explicit Window.window_index placement
tony Jun 27, 2026
4a1428d
Mcp(feat): Expose build_workspace on the async server
tony Jun 27, 2026
81219ae
Mcp(feat[safety]): Add tier constants, resolver, ExpectedToolError
tony Jun 27, 2026
bd1290c
Mcp(feat[middleware]): Port error-result + tail-preserving limiter
tony Jun 27, 2026
6f2cae5
Mcp(feat[middleware]): Port safety, audit, readonly-retry middleware
tony Jun 27, 2026
b82c1b5
Mcp(feat[safety]): Wire safety gate + middleware into the builders
tony Jun 27, 2026
16531dc
Mcp(feat[prompts]): Add recipe prompts in engine-ops vocabulary
tony Jun 27, 2026
4f2b0c5
Mcp(feat[resources]): Add tmux:// hierarchy resources over the engine
tony Jun 27, 2026
ea4def1
Mcp(feat[lifespan]): Add engine-probe lifespan (async server)
tony Jun 27, 2026
f08db55
Workspace(feat[cli]): Add `load` command for .tmuxp.yaml files
tony Jun 27, 2026
108ef17
Workspace(feat): blank/pane empty-pane parity + cli --dry-run
tony Jun 27, 2026
81c4e14
Workspace(fix): First window's start_directory for its first pane
tony Jun 27, 2026
5fe8bb5
Ops(feat): Per-step host hook and bounded planner
tony Jun 27, 2026
31f3a72
Workspace(feat): Fold build dispatches by default
tony Jun 27, 2026
92943fb
Workspace(feat): Fold --dry-run output
tony Jun 27, 2026
7f11fb1
Ops(feat[new_pane]): Add floating pane operation
tony Jun 27, 2026
af8333d
Ops(fix[break_pane]): Work around tmux 3.7 break-pane crash
tony Jun 27, 2026
5e86b22
Engines(feat): Resolve engine tmux version for gating
tony Jun 27, 2026
0d20662
Workspace(feat[ir]): Add floating-pane declarations
tony Jun 27, 2026
2b1b314
Workspace(feat[compiler]): Build floating panes from specs
tony Jun 27, 2026
fa024a5
Workspace(feat[compiler]): Cross-window floats via symbol table
tony Jun 27, 2026
36d064e
Query(feat): Add snapshot-backed live pane query
tony Jun 27, 2026
5873ece
Query(feat): Add per-pane command building that folds
tony Jun 27, 2026
9925fd8
Facade(feat[pane]): Add new_pane floating parity
tony Jun 27, 2026
25a333e
Mcp(feat[pane]): Add curated new_pane floating tool
tony Jun 27, 2026
8cc479f
Mcp(feat[registry]): Surface whole-op min_version in schema
tony Jun 27, 2026
5cc6840
Mcp(fix[prompts]): wait_for_output uses target=
tony Jun 28, 2026
34417ed
Workspace(fix[analyze]): Reject bad command items
tony Jun 28, 2026
1e44f50
Workspace(fix[env]): Inherit window env in splits
tony Jun 28, 2026
c3a7658
docs(CHANGES) Note on updates
tony Jul 4, 2026
be3c967
Mcp(refactor): Drop dead is_conservative_caller
tony Jul 4, 2026
e1dc23e
Engines(fix): Report tmux version over control mode
tony Jul 4, 2026
46aaea3
Ops(docs): Fix stale fold comment in chain test
tony Jul 4, 2026
2a28ff6
Models(feat): Add PaneSnapshot.floating flag
tony Jul 4, 2026
3e03cba
Engines(fix): Reap control-mode phantom sessions
tony Jul 4, 2026
4223b7c
Engines(fix): Close subscribers on engine death
tony Jul 4, 2026
9c16bb4
Query(feat): Split-type forward-ref pane handles
tony Jul 4, 2026
09d3121
Fluent(feat): Add plan() forward-ref build tier
tony Jul 4, 2026
ef95643
Workspace(feat): Add freeze (live server to IR)
tony Jul 4, 2026
ecfb3d1
Workspace(feat): Add variant expand + workspace sets
tony Jul 4, 2026
c71c090
Ops(feat): Add plan explain() and astream()
tony Jul 4, 2026
b41069d
Fluent(feat): Add sleep/wait host boundaries
tony Jul 4, 2026
609a226
Mcp(feat): Add explain_plan tool
tony Jul 4, 2026
ab12f58
Ops(feat): Add conditional find-or-create via ensure()
tony Jul 4, 2026
a0d89e2
Fluent(feat): Add find_or_create_session
tony Jul 4, 2026
0d412cf
Test(fix): Clean up sessions in fluent live tests
tony Jul 4, 2026
4f823f4
docs(experimental): Document the fluent plan() builder
tony Jul 4, 2026
c94cd96
Mcp(fix): Preserve ensure across plan-tool serialization
tony Jul 4, 2026
659a372
Ops(test): Make the ensure probe test format-honest
tony Jul 4, 2026
b98faab
Workspace(docs): on_event and astream caveats
tony Jul 4, 2026
2641767
Workspace(fix): Stringify option/env values
tony Jul 5, 2026
7ca6f70
Ops(fix): Skip marked fold for detached creators
tony Jul 5, 2026
016624d
Engines(fix): End subscribe() after engine death
tony Jul 5, 2026
61a7eb2
Query(refactor): Namespace dataclasses.replace
tony Jul 5, 2026
9cd83b2
docs(experimental): Drop prototype lineage
tony Jul 5, 2026
a502350
Workspace(fix): Honor window_shell on window 0
tony Jul 5, 2026
7b9ae1e
Workspace(fix): wait_pane uses effective shell
tony Jul 5, 2026
0b33113
docs(experimental): Add arun/aexecute doctests
tony Jul 5, 2026
8de89a1
Facade(fix): Add AsyncWindow.select_layout
tony Jul 5, 2026
29f4d56
Engines(fix): ImsgEngine reports tmux version
tony Jul 5, 2026
da107b6
Mcp(fix): Gate wait_for_output prompts on events
tony Jul 5, 2026
06bfa68
Mcp(fix): Redact non-str sensitive audit args
tony Jul 5, 2026
928d2e9
docs(experimental): Trim more branch narrative
tony Jul 5, 2026
27c601f
Mcp(docs): Fix build_workspace registration claim
tony Jul 5, 2026
5db130d
Workspace(fix): Confirm cwd on the first pane
tony Jul 5, 2026
f7d7929
Engines(feat[async_control_mode]): Add supervised reconnect
tony Jul 5, 2026
7dcf925
Mcp(fix[events]): Restart the event drain after reconnect
tony Jul 5, 2026
8c31bd5
Objects(refactor): Rename facade package to objects
tony Jul 5, 2026
17a2125
Engines(fix[async_control_mode]): Escalate backoff on connect-then-die
tony Jul 5, 2026
a1182ab
Engines(fix[async_control_mode]): Terminate the prior proc on reconnect
tony Jul 5, 2026
4873bb9
Scripts(feat[bench]): Add hermetic engine build-benchmark grid
tony Jul 5, 2026
b89c87d
Scripts(docs[bench]): Add engine benchmark results
tony Jul 5, 2026
719d996
Scripts(fix[bench]): Make bench_engines mypy-clean under `mypy .`
tony Jul 5, 2026
1e405cc
Engines(refactor[mock]): Rename ConcreteEngine to MockEngine
tony Jul 5, 2026
5896c50
Engines(refactor[connection]): Hold a ServerConnection
tony Jul 11, 2026
8aea17b
Chain(fix[_chain]): Wire the fold guard, import the rule
tony Jul 11, 2026
81850d0
Experimental(refactor[dedup]): Import instead of re-inline
tony Jul 11, 2026
10445e0
Experimental(refactor[dead-code]): Delete unread surface
tony Jul 11, 2026
acb1b5b
Engines(feat[control-mode]): Add unescape_control_output
tony Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,88 @@ $ uvx --from 'libtmux' --prerelease allow python
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### What's new

#### Experimental operations and engines (#690)

Operations describe tmux commands as data. Each renders its argv against a tmux
version (dropping flags an older tmux cannot accept), adapts raw output into a
typed result, and serializes to and from plain dicts -- all without a running
tmux server. The set spans the read seam (``list-*``, ``has-session``,
``display-message``, ``show-options``, ``show-buffer``) and the
mutating/creating surface for panes, windows, the server, options, environment,
hooks, and paste buffers. A registry-generated catalog on the {ref}`experimental`
page always matches the code.

Engines run those operations behind one protocol, so the same operation returns
the same typed result whether it goes through a subprocess (the classic path
that reproduces today's libtmux behavior), an in-memory simulator for tests and
dry runs, a persistent ``tmux -C`` control connection, an async transport, or
tmux's native binary peer protocol. Results never raise on construction;
raising is opt-in via ``raise_for_status()``, and how a failed result is handled
is each engine's policy.

A {class}`~libtmux.experimental.ops.plan.LazyPlan` records operations and yields
forward references so a later operation can target an object that does not exist
yet, resolved against captured ids at execution time. How a plan becomes tmux
dispatches is a pluggable {class}`~libtmux.experimental.ops.planner.Planner`
(sequential, ``;``-folding, or ``{marked}``-folding), so dispatch strategies can
be A/B tested against the same plan with identical results.

#### Declarative workspace builds fold to a few tmux calls (#690)

A {class}`~libtmux.experimental.workspace.ir.Workspace` declares a session as a
tree of windows and panes and lowers to a Core
{class}`~libtmux.experimental.ops.plan.LazyPlan`, so a tmuxp-style spec can be
analyzed, inspected, and built over any engine.
{meth}`~libtmux.experimental.workspace.ir.Workspace.build` and its async twin
{meth}`~libtmux.experimental.workspace.ir.Workspace.abuild` fold the build's
dispatches by default: a multi-pane window collapses from one tmux call per
operation into a handful of ``;``-chained and ``{marked}`` dispatches, so a
session renders in a few round-trips instead of dozens.

The resulting {class}`~libtmux.experimental.ops.plan.PlanResult` is identical to
an unfolded build -- only the dispatch count changes -- because host-side steps
(per-command sleeps, the ``wait_pane`` anti-race, ``before_script``) stay hard
fold boundaries that a fold never crosses. Pass a
{class}`~libtmux.experimental.ops.planner.SequentialPlanner` to ``build`` for one
legible tmux call per operation when debugging.

#### Floating panes on tmux 3.7 (#690)

On tmux 3.7, the operations create floating panes -- overlays with an absolute
size, position, and optional zoom. A ``new-pane`` operation, ``new_pane()`` on
the pane wrappers, and a curated MCP tool each open one, and a
{class}`~libtmux.experimental.workspace.ir.Workspace` can declare floating panes
on a window, including a pane that overlays a different window.

#### Query and command live panes with `panes()` (#690)

{func}`~libtmux.experimental.query.panes` is a lazy, chainable query over the
panes a running server has: ``filter``, ``order_by``, ``limit``, and ``map``
compose and read nothing until a terminal call. The same query commands what it
selects -- ``commands()`` attaches per-pane actions (send keys, resize, select,
respawn, clear history, kill) that run as one folded tmux dispatch. A query
resolves against a live engine or a plain list of pane snapshots, so the same
code runs offline in tests.

#### Drive tmux from an MCP server (#690)

An optional Model Context Protocol server exposes tmux as tools an AI agent can
call, installed with the ``libtmux[mcp]`` extra and launched as
``libtmux-engine-mcp``. It offers a curated vocabulary of intuitive verbs
(``send_input``, ``wait_for_output``, ``split_pane``, ``capture_pane``,
``new_pane``, and the session, window, and pane lifecycle), a tool for every
individual operation, and plan tools that preview or build a whole workspace in
one call.

The server is caller-aware: because a control-mode agent shares the server with
the panes it drives, it knows which pane launched it and refuses to kill or
respawn its own pane, window, or session. A safety level (``LIBTMUX_SAFETY``)
keeps mutating and destructive tools hidden until an operator opts in, and a
needle-free ``wait_for_output`` reports when a pane goes quiet after a command --
no sentinel string -- and whether its process exited.

## libtmux 0.62.0 (2026-07-12)

libtmux 0.62.0 teaches libtmux objects to locate themselves and to resolve
Expand Down
120 changes: 120 additions & 0 deletions docs/_ext/tmuxop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Sphinx directive that renders the experimental operation catalog.

``.. tmuxop-catalog::`` (or the MyST fenced form) walks
:func:`libtmux.experimental.ops.catalog` and emits a table of operations with
their scope, safety tier, result type, minimum tmux version, and summary. The
operation registry is the single source of truth, so the rendered reference
cannot drift from the code.

Options
-------
``:scope:`` / ``:safety:``
Filter to one scope (``pane``/``window``/``session``/``server``/``client``)
or safety tier (``readonly``/``mutating``/``destructive``).
``:primitive-only:``
Show only operations that wrap a single tmux command.

This is the in-repo renderer; a full gp-sphinx ``tmuxop`` domain (cross-reference
roles + an operations index) can later replace it under the same directive name.
"""

from __future__ import annotations

import typing as t

from docutils import nodes
from docutils.parsers.rst import directives
from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective

if t.TYPE_CHECKING:
from collections.abc import Sequence

from sphinx.application import Sphinx

logger = logging.getLogger(__name__)

_HEADERS = ("Operation", "Command", "Scope", "Safety", "Result", "Min tmux", "Summary")


def _row(cells: Sequence[str]) -> nodes.row:
"""Build a docutils table row from string cells."""
row = nodes.row()
for cell in cells:
entry = nodes.entry()
entry += nodes.paragraph(text=cell)
row += entry
return row


def _table(headers: Sequence[str], rows: Sequence[Sequence[str]]) -> nodes.table:
"""Build a simple docutils table."""
table = nodes.table()
tgroup = nodes.tgroup(cols=len(headers))
table += tgroup
for _ in headers:
tgroup += nodes.colspec(colwidth=1)
thead = nodes.thead()
thead += _row(headers)
tgroup += thead
tbody = nodes.tbody()
for row in rows:
tbody += _row(row)
tgroup += tbody
return table


class TmuxopCatalogDirective(SphinxDirective):
"""Render the operation catalog as a table."""

has_content = False
option_spec: t.ClassVar[dict[str, t.Any]] = {
"scope": directives.unchanged,
"safety": directives.unchanged,
"primitive-only": directives.flag,
}

def run(self) -> list[nodes.Node]:
"""Build the catalog table from the operation registry."""
from libtmux.experimental.ops import catalog

entries = catalog()
scope = self.options.get("scope")
safety = self.options.get("safety")
if scope:
entries = [entry for entry in entries if entry.scope == scope]
if safety:
entries = [entry for entry in entries if entry.safety == safety]
if "primitive-only" in self.options:
entries = [entry for entry in entries if entry.primitive]

if not entries:
logger.warning(
"tmuxop-catalog: no operations matched the given filters",
location=self.get_location(),
)
return []

rows = [
(
entry.kind,
entry.command,
entry.scope,
entry.safety,
entry.result_type,
entry.min_version or "-",
entry.summary,
)
for entry in entries
]
return [_table(_HEADERS, rows)]


def setup(app: Sphinx) -> dict[str, t.Any]:
"""Register the directive."""
app.add_directive("tmuxop-catalog", TmuxopCatalogDirective)
return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
project_src = project_root / "src"

sys.path.insert(0, str(project_src))
sys.path.insert(0, str(cwd / "_ext"))

# package data
about: dict[str, str] = {}
Expand All @@ -34,6 +35,7 @@
"sphinx_autodoc_api_style",
"sphinx_autodoc_pytest_fixtures",
"sphinx.ext.todo",
"tmuxop",
],
intersphinx_mapping={
"python": ("https://docs.python.org/", None),
Expand Down
Loading