Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
000784f
Ops(feat): Add typed operation and engine spine
tony Jun 21, 2026
160af41
Ops(feat): Add classic + concrete engines and contract suite
tony Jun 21, 2026
6d998ac
Ops(feat): Add async engine, lazy plans, and op catalog
tony Jun 21, 2026
f7d2ebc
Ops(feat): Add persistent control-mode engine
tony Jun 21, 2026
320d589
Ops(feat): Add eager + lazy pane facades over the spine
tony Jun 21, 2026
950397e
Ops(feat): Add async control-mode + concrete engines
tony Jun 21, 2026
50965d1
Ops(feat): Add AckResult for no-output operations
tony Jun 21, 2026
0d23fc0
Models(feat): Add pure object-graph snapshots
tony Jun 21, 2026
7608ebd
Ops(feat): Add read-seam list operations
tony Jun 21, 2026
7a596a9
docs(experimental): Add tmuxop-catalog directive
tony Jun 21, 2026
37a3557
ControlMode(fix): Consume startup ACK, drain unsolicited blocks
tony Jun 21, 2026
b071de5
Ops(feat): Add lazy-plan chainability (>> and ; folding)
tony Jun 21, 2026
8546098
Facade(feat): Add Server/Session/Window facades + creation ops
tony Jun 21, 2026
50cd559
Imsg(feat): Add native imsg engine + live parity test
tony Jun 21, 2026
8e3ddde
Facade(feat): Complete the facade matrix (Server/Session/Client)
tony Jun 21, 2026
9d378b6
chore(deps[dev]): Add ty type checker + config
tony Jun 21, 2026
51e0248
Ops(feat): Add pluggable planners + {marked} fold
tony Jun 21, 2026
823b797
Ops(feat): Add non-list read operations
tony Jun 21, 2026
44d40c2
Ops(feat): Add pane mutation/creation operations
tony Jun 21, 2026
ad76e13
Ops(feat): Add window mutation/navigation operations
tony Jun 21, 2026
7420f53
Ops(feat): Add server/option/environment operations
tony Jun 21, 2026
54f8416
Ops(feat): Add paste-buffer operations
tony Jun 21, 2026
f24e78f
docs(experimental): Document engines and lazy plans
tony Jun 21, 2026
9f13041
docs(CHANGES): Note experimental operations and engines
tony Jun 21, 2026
6845fe0
Ops(fix): Correct move-window -k and paste-buffer -r
tony Jun 21, 2026
539cfd3
Ops(fix): Resolve SlotRef src_target in lazy plans
tony Jun 21, 2026
e85308f
Ops(fix): Skip all decorates when a marked-fold create fails
tony Jun 21, 2026
1b0cd7b
Ops(fix): Mark save-buffer readonly to match its effects
tony Jun 21, 2026
cdd91c0
Ops(docs): Fix PipePane parameter name in docstring
tony Jun 21, 2026
3028c88
Ops(fix): Log imsg argv as a scalar tmux_cmd field
tony Jun 21, 2026
ab4ed91
Ops(docs): Add doctests to the planner plan() methods
tony Jun 21, 2026
6fd18fe
Ops(fix): Resolve decorate src_target in {marked} folds
tony Jun 21, 2026
874a673
Ops(fix): Keep ; a bare separator in control-mode engines
tony Jun 21, 2026
f26f665
Ops(fix): Treat a blank captured id as no id in marked folds
tony Jun 21, 2026
0a573d0
Ops(fix): Complete a marked fold whose creator does not capture
tony Jun 21, 2026
ad0228a
Ops(fix): Drop create stdout when attributing marked decorates
tony Jun 21, 2026
f1d5898
Ops(fix): Target the concrete pane in marked decorate results
tony Jun 21, 2026
b0e1dec
Ops(fix): Decode SubprocessEngine output as UTF-8
tony Jun 21, 2026
be5e16f
Models(refactor): Use namespaced dataclasses.replace in snapshots
tony Jun 21, 2026
22d9946
Ops(docs): Fix PipePane -o flag description
tony Jun 21, 2026
344d90f
Ops(fix): Mark save-buffer mutating (it writes a file)
tony Jun 21, 2026
d313589
Ops(fix): Correlate control-mode blocks per command and by flags
tony Jun 21, 2026
c5a4ec1
Ops(fix): Clear pending futures on async control-mode write failure
tony Jun 21, 2026
31b7147
Ops(fix): Suppress ProcessLookupError on async cancel terminate
tony Jun 21, 2026
7268276
Engines(fix): Remove the unreachable asyncio engine kind
tony Jun 21, 2026
73b2bdb
Ops(fix): Normalize tmux master version in operation gates
tony Jun 21, 2026
d458cc3
Ops(fix): Reject SendKeys literal+enter combination
tony Jun 21, 2026
2e87cb7
Ops(fix): Keep all lines of a display-message result
tony Jun 21, 2026
2e4f822
Ops(fix): Centralize the has-session stderr->stdout fold in the op
tony Jun 21, 2026
2662e16
Engines(docs): Note ConcreteEngine query-simulation limits
tony Jun 22, 2026
54b409b
Engines(fix): Avoid imsg UnboundLocalError on socket() failure
tony Jun 22, 2026
1892f8e
Engines(fix): Return imsg exit result on clean close after MSG_EXIT
tony Jun 22, 2026
74bc792
Engines(fix): Close imsg dup'd fds if the identify send never happens
tony Jun 22, 2026
9becbbd
Imsg(fix): Send identify LONGFLAGS frame once
tony Jun 22, 2026
1c7c161
Engines(test): Widen async control-mode coverage
tony Jun 22, 2026
8e8f25e
Ops(feat[send_keys]): Add suppress_history flag
tony Jun 22, 2026
c449e4c
Ops(feat): Capture implicit child ids on create
tony Jun 22, 2026
b32c4d4
Workspace(feat): Declarative WorkspaceBuilder on the typed-ops Core
tony Jun 22, 2026
0a0b454
Ops(feat): Serialize bindings + add plan preview
tony Jun 22, 2026
023df68
Mcp(feat): Add framework-agnostic tool projection
tony Jun 23, 2026
09b254e
Mcp(feat): Add optional fastmcp adapter (libtmux[mcp])
tony Jun 23, 2026
e80f875
Mcp(feat): Per-op + plan tools and a stdio server
tony Jun 23, 2026
8b60d80
Mcp(feat): Port mcp_swap config-swap dev script
tony Jun 23, 2026
81bff83
Tests(chore): Run the mcp adapter suite in the gate
tony Jun 23, 2026
b532b8e
Workspace(test): Cover analyzer, compiler, runner
tony Jun 23, 2026
8c0e5a0
Mcp(feat): Add grok + agy CLIs to mcp_swap
tony Jun 23, 2026
bdda33c
Mcp(feat): Caller-aware async tmux tool surface
tony Jun 23, 2026
51ae47f
Mcp(feat): Caller discovery + self-kill guards
tony Jun 23, 2026
2566e0a
Mcp(fix): Harden self-kill guards + socket scoping
tony Jun 23, 2026
9a13fd0
Mcp(feat): Needle-free pane-output monitor
tony Jun 23, 2026
98cd4f2
Mcp(feat): Make wait_for_output discoverable
tony Jun 24, 2026
6b7978d
Mcp(fix): Close self-kill guard deferrals
tony Jun 24, 2026
ced4cec
Mcp(fix): Harden wait_for_output monitor
tony Jun 24, 2026
f43210a
Workspace(feat): Thread env/shell/options through declarative tier
tony Jun 27, 2026
023b907
Workspace(feat): Add per-command Command (enter + sleeps)
tony Jun 27, 2026
7750a01
Ops(feat): Add ForwardCaptureError + ShowOptionsResult.get_int
tony Jun 27, 2026
23f579c
Workspace(feat): Add Workspace.to_dict + read suppress_history
tony Jun 27, 2026
4e4a50b
Workspace(feat): Add BuildEvent stream + on_event observer
tony Jun 27, 2026
6bc9881
Workspace(feat): Add opt-in wait_pane readiness (anti-race)
tony Jun 27, 2026
351eb34
Workspace(feat): Honor explicit Window.window_index placement
tony Jun 27, 2026
64d02d1
Mcp(feat): Expose build_workspace on the async server
tony Jun 27, 2026
43866de
Mcp(feat[safety]): Add tier constants, resolver, ExpectedToolError
tony Jun 27, 2026
abf3b66
Mcp(feat[middleware]): Port error-result + tail-preserving limiter
tony Jun 27, 2026
bb745c4
Mcp(feat[middleware]): Port safety, audit, readonly-retry middleware
tony Jun 27, 2026
b5d48a8
Mcp(feat[safety]): Wire safety gate + middleware into the builders
tony Jun 27, 2026
ce43a23
Mcp(feat[prompts]): Add recipe prompts in engine-ops vocabulary
tony Jun 27, 2026
eb944e4
Mcp(feat[resources]): Add tmux:// hierarchy resources over the engine
tony Jun 27, 2026
595b435
Mcp(feat[lifespan]): Add engine-probe lifespan (async server)
tony Jun 27, 2026
c4e3acd
Workspace(feat[cli]): Add `load` command for .tmuxp.yaml files
tony Jun 27, 2026
b0964c2
Workspace(feat): blank/pane empty-pane parity + cli --dry-run
tony Jun 27, 2026
06e9760
Workspace(fix): First window's start_directory for its first pane
tony Jun 27, 2026
6b847ed
Ops(feat): Per-step host hook and bounded planner
tony Jun 27, 2026
7308967
Workspace(feat): Fold build dispatches by default
tony Jun 27, 2026
b9ed1f9
Workspace(feat): Fold --dry-run output
tony Jun 27, 2026
5521f16
Ops(feat[new_pane]): Add floating pane operation
tony Jun 27, 2026
105c5a3
Ops(fix[break_pane]): Work around tmux 3.7 break-pane crash
tony Jun 27, 2026
b5ee3a0
Engines(feat): Resolve engine tmux version for gating
tony Jun 27, 2026
51a42cf
Workspace(feat[ir]): Add floating-pane declarations
tony Jun 27, 2026
dd976a9
Workspace(feat[compiler]): Build floating panes from specs
tony Jun 27, 2026
9c0a957
Workspace(feat[compiler]): Cross-window floats via symbol table
tony Jun 27, 2026
b86f815
Query(feat): Add snapshot-backed live pane query
tony Jun 27, 2026
7de5b1c
Query(feat): Add per-pane command building that folds
tony Jun 27, 2026
bd082a2
Facade(feat[pane]): Add new_pane floating parity
tony Jun 27, 2026
e54a904
Mcp(feat[pane]): Add curated new_pane floating tool
tony Jun 27, 2026
06b1969
Mcp(feat[registry]): Surface whole-op min_version in schema
tony Jun 27, 2026
798d413
Mcp(fix[prompts]): wait_for_output uses target=
tony Jun 28, 2026
409d064
Workspace(fix[analyze]): Reject bad command items
tony Jun 28, 2026
f542fb2
Workspace(fix[env]): Inherit window env in splits
tony Jun 28, 2026
4cfdd2d
docs(CHANGES) Note on updates
tony Jul 4, 2026
e1e854a
Mcp(refactor): Drop dead is_conservative_caller
tony Jul 4, 2026
d27b1b9
Engines(fix): Report tmux version over control mode
tony Jul 4, 2026
952f1f5
Ops(docs): Fix stale fold comment in chain test
tony Jul 4, 2026
c983bb1
Models(feat): Add PaneSnapshot.floating flag
tony Jul 4, 2026
805c029
Engines(fix): Reap control-mode phantom sessions
tony Jul 4, 2026
2dfa992
Engines(fix): Close subscribers on engine death
tony Jul 4, 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.61.0 (2026-07-04)

libtmux 0.61.0 hardens support for the tmux 3.7 patch line. It fixes
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
136 changes: 136 additions & 0 deletions docs/experimental.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
(experimental)=

# Experimental: operations & engines

```{warning}
Everything under {mod}`libtmux.experimental` is **not** covered by the
versioning policy and may change or be removed between any two releases.
```

`libtmux.experimental` hosts an inert, typed *operation* substrate and the
*engines* that execute it. An operation describes a tmux command (it renders
argv, carries its result type and metadata, and serializes) without dispatching;
an engine runs operations and returns typed results. The same operation returns
the same typed result whether executed by a subprocess, an in-memory simulator,
a persistent `tmux -C` control connection, or an async transport.

See ``tmux-python/libtmux`` issue 689 for the operationalization plan.

## Running an operation

An operation is a value; ``run`` (or ``arun`` for async) hands it to an engine
and returns the engine's typed result. Results never raise on construction --
inspect ``ok``/``status``, or opt into raising with ``raise_for_status()``:

```python
>>> from libtmux.experimental.ops import HasSession, run
>>> from libtmux.experimental.ops._types import SessionId
>>> from libtmux.experimental.engines import ConcreteEngine
>>> result = run(HasSession(target=SessionId("$0")), ConcreteEngine())
>>> result.ok
True
>>> result.raise_for_status() is result
True
```

How a *failed* result is treated is the engine's policy: the classic subprocess
path raises in its facade to match today's libtmux behavior, while the newer
engines hand the result back and let the caller decide.

## Choosing an engine

Every engine satisfies the same ``TmuxEngine`` (or ``AsyncTmuxEngine``)
protocol, so swapping engines never changes an operation or its result type --
only *how* and *where* the command runs.

| Engine | Transport | Use it for |
| --- | --- | --- |
| ``SubprocessEngine`` | one ``tmux`` process per command | the classic path; reproduces today's libtmux behavior |
| ``ConcreteEngine`` | in-memory, no tmux | tests and dry runs (deterministic, fabricated output) |
| ``ControlModeEngine`` | a persistent ``tmux -C`` connection | many commands over one long-lived session |
| ``ImsgEngine`` | tmux's native binary peer protocol | an opt-in easter egg |

Each has an ``Async*`` counterpart (``AsyncSubprocessEngine``,
``AsyncConcreteEngine``, ``AsyncControlModeEngine``) behind ``AsyncTmuxEngine``.
Construct one directly, bind it to a live server with
``SubprocessEngine.for_server(server)``, or select one by name from the engine
registry:

```python
>>> from libtmux.experimental.engines import available_engines, create_engine
>>> from libtmux.experimental.ops import HasSession, run
>>> from libtmux.experimental.ops._types import SessionId
>>> available_engines()
('concrete', 'control_mode', 'imsg', 'subprocess')
>>> engine = create_engine("concrete")
>>> run(HasSession(target=SessionId("$0")), engine).status
'complete'
```

## Lazy plans and planners

A {class}`~libtmux.experimental.ops.plan.LazyPlan` records operations without
running them, returning a forward *slot reference* for each created object so a
later operation can target something that does not exist yet. ``execute``
(or ``aexecute``) resolves those references against captured ids as it goes:

```python
>>> from libtmux.experimental.ops import LazyPlan, SplitWindow, SendKeys
>>> from libtmux.experimental.ops._types import WindowId
>>> from libtmux.experimental.engines import ConcreteEngine
>>> plan = LazyPlan()
>>> pane = plan.add(SplitWindow(target=WindowId("@1")))
>>> _ = plan.add(SendKeys(target=pane, keys="echo hi", enter=True))
>>> outcome = plan.execute(ConcreteEngine())
>>> outcome.ok
True
>>> [r.status for r in outcome.results]
['complete', 'complete']
```

Operations also compose with ``>>`` into a chain, which a plan can run as one
dispatch when the members are chainable.

*How* a plan turns into dispatches is a pluggable
{class}`~libtmux.experimental.ops.planner.Planner`, so strategies can be A/B
tested against the same plan:

- ``SequentialPlanner`` -- one dispatch per operation (the default).
- ``FoldingPlanner`` -- folds adjacent chainable operations into a single
``;``-separated dispatch.
- ``MarkedPlanner`` -- folds a "create then decorate the new pane" run into one
dispatch using tmux's ``{marked}`` register.

Every planner produces the same per-operation result; they differ only in how
many times tmux is invoked:

```python
>>> from libtmux.experimental.ops import LazyPlan, SplitWindow, SendKeys, FoldingPlanner
>>> from libtmux.experimental.ops._types import WindowId
>>> from libtmux.experimental.engines import ConcreteEngine
>>> plan = LazyPlan()
>>> pane = plan.add(SplitWindow(target=WindowId("@1")))
>>> _ = plan.add(SendKeys(target=pane, keys="echo hi", enter=True))
>>> plan.execute(ConcreteEngine(), planner=FoldingPlanner()).ok
True
```

## Operation catalog

The catalog below is generated from the operation registry, so it always matches
the code.

```{tmuxop-catalog}
```

### Read-only operations

```{tmuxop-catalog}
:safety: readonly
```

### Destructive operations

```{tmuxop-catalog}
:safety: destructive
```
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ api/index
api/testing/index
internals/index
project/index
experimental
history
migration
glossary
Expand Down
9 changes: 7 additions & 2 deletions docs/topics/automation_patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,18 @@ command that never finishes can't hang your script forever. The `poll_interval`
the latency/work trade in one knob: poll faster to react sooner, slower to spare tmux
the round-trips.

> **Note:** This polls with `capture_pane` + `sleep` — correct for the
> synchronous library. If you drive tmux through the libtmux MCP server, prefer
> the event-backed `wait_for_output` tool instead: it folds live `%output` and
> returns when the pane settles, with no polling.

```python
>>> import time

>>> monitor_window = session.new_window(window_name='monitor', attach=False)
>>> monitor_pane = monitor_window.active_pane

>>> def wait_for_output(pane, text, timeout=5.0, poll_interval=0.1):
>>> def wait_for_text(pane, text, timeout=5.0, poll_interval=0.1):
... """Wait for specific text to appear in pane output."""
... start = time.time()
... while time.time() - start < timeout:
Expand All @@ -135,7 +140,7 @@ the round-trips.
... return False

>>> monitor_pane.send_keys('sleep 0.2; echo "READY"')
>>> wait_for_output(monitor_pane, 'READY', timeout=2.0)
>>> wait_for_text(monitor_pane, 'READY', timeout=2.0)
True

>>> # Clean up
Expand Down
Loading
Loading