Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
154 commits
Select commit Hold shift + click to select a range
049363f
Ops(feat): Add typed operation and engine spine
tony Jun 21, 2026
da87c7b
Ops(feat): Add classic + concrete engines and contract suite
tony Jun 21, 2026
ad074d1
Ops(feat): Add async engine, lazy plans, and op catalog
tony Jun 21, 2026
4478d36
Ops(feat): Add persistent control-mode engine
tony Jun 21, 2026
b1dcf85
Ops(feat): Add eager + lazy pane facades over the spine
tony Jun 21, 2026
ade0448
Ops(feat): Add async control-mode + concrete engines
tony Jun 21, 2026
c98b479
Ops(feat): Add AckResult for no-output operations
tony Jun 21, 2026
c8c69f8
Models(feat): Add pure object-graph snapshots
tony Jun 21, 2026
f98a720
Ops(feat): Add read-seam list operations
tony Jun 21, 2026
1205c12
docs(experimental): Add tmuxop-catalog directive
tony Jun 21, 2026
72207f9
ControlMode(fix): Consume startup ACK, drain unsolicited blocks
tony Jun 21, 2026
4fbaeb4
Ops(feat): Add lazy-plan chainability (>> and ; folding)
tony Jun 21, 2026
21b59e7
Facade(feat): Add Server/Session/Window facades + creation ops
tony Jun 21, 2026
8ddf760
Imsg(feat): Add native imsg engine + live parity test
tony Jun 21, 2026
f901372
Facade(feat): Complete the facade matrix (Server/Session/Client)
tony Jun 21, 2026
55c3d89
chore(deps[dev]): Add ty type checker + config
tony Jun 21, 2026
5abbfc7
Ops(feat): Add pluggable planners + {marked} fold
tony Jun 21, 2026
f03747a
Ops(feat): Add non-list read operations
tony Jun 21, 2026
37ce66e
Ops(feat): Add pane mutation/creation operations
tony Jun 21, 2026
eb3a400
Ops(feat): Add window mutation/navigation operations
tony Jun 21, 2026
27ac248
Ops(feat): Add server/option/environment operations
tony Jun 21, 2026
a22ee7f
Ops(feat): Add paste-buffer operations
tony Jun 21, 2026
da3ecbd
docs(experimental): Document engines and lazy plans
tony Jun 21, 2026
2472522
docs(CHANGES): Note experimental operations and engines
tony Jun 21, 2026
e0b7c48
Ops(fix): Correct move-window -k and paste-buffer -r
tony Jun 21, 2026
93c8991
Ops(fix): Resolve SlotRef src_target in lazy plans
tony Jun 21, 2026
5865c7a
Ops(fix): Skip all decorates when a marked-fold create fails
tony Jun 21, 2026
2a3130e
Ops(fix): Mark save-buffer readonly to match its effects
tony Jun 21, 2026
fa5749a
Ops(docs): Fix PipePane parameter name in docstring
tony Jun 21, 2026
dadfad8
Ops(fix): Log imsg argv as a scalar tmux_cmd field
tony Jun 21, 2026
0b16a5a
Ops(docs): Add doctests to the planner plan() methods
tony Jun 21, 2026
2606c3a
Ops(fix): Resolve decorate src_target in {marked} folds
tony Jun 21, 2026
2df4186
Ops(fix): Keep ; a bare separator in control-mode engines
tony Jun 21, 2026
3842204
Ops(fix): Treat a blank captured id as no id in marked folds
tony Jun 21, 2026
8b9f49c
Ops(fix): Complete a marked fold whose creator does not capture
tony Jun 21, 2026
85493c3
Ops(fix): Drop create stdout when attributing marked decorates
tony Jun 21, 2026
25c4fed
Ops(fix): Target the concrete pane in marked decorate results
tony Jun 21, 2026
0392f72
Ops(fix): Decode SubprocessEngine output as UTF-8
tony Jun 21, 2026
6663357
Models(refactor): Use namespaced dataclasses.replace in snapshots
tony Jun 21, 2026
81310e2
Ops(docs): Fix PipePane -o flag description
tony Jun 21, 2026
0cf925e
Ops(fix): Mark save-buffer mutating (it writes a file)
tony Jun 21, 2026
0f139d7
Ops(fix): Correlate control-mode blocks per command and by flags
tony Jun 21, 2026
abd8814
Ops(fix): Clear pending futures on async control-mode write failure
tony Jun 21, 2026
7d06535
Ops(fix): Suppress ProcessLookupError on async cancel terminate
tony Jun 21, 2026
05afaa2
Engines(fix): Remove the unreachable asyncio engine kind
tony Jun 21, 2026
a7566c8
Ops(fix): Normalize tmux master version in operation gates
tony Jun 21, 2026
0bf9d6f
Ops(fix): Reject SendKeys literal+enter combination
tony Jun 21, 2026
9752624
Ops(fix): Keep all lines of a display-message result
tony Jun 21, 2026
b8c19ef
Ops(fix): Centralize the has-session stderr->stdout fold in the op
tony Jun 21, 2026
f16a681
Engines(docs): Note ConcreteEngine query-simulation limits
tony Jun 22, 2026
48c08b3
Engines(fix): Avoid imsg UnboundLocalError on socket() failure
tony Jun 22, 2026
4a2d602
Engines(fix): Return imsg exit result on clean close after MSG_EXIT
tony Jun 22, 2026
1ef87ab
Engines(fix): Close imsg dup'd fds if the identify send never happens
tony Jun 22, 2026
43ff301
Imsg(fix): Send identify LONGFLAGS frame once
tony Jun 22, 2026
10582d4
Engines(test): Widen async control-mode coverage
tony Jun 22, 2026
acff025
Ops(feat[send_keys]): Add suppress_history flag
tony Jun 22, 2026
2e592d1
Ops(feat): Capture implicit child ids on create
tony Jun 22, 2026
a0d3baa
Workspace(feat): Declarative WorkspaceBuilder on the typed-ops Core
tony Jun 22, 2026
325977e
Ops(feat): Serialize bindings + add plan preview
tony Jun 22, 2026
da89b73
Mcp(feat): Add framework-agnostic tool projection
tony Jun 23, 2026
bb4b296
Mcp(feat): Add optional fastmcp adapter (libtmux[mcp])
tony Jun 23, 2026
307b5d1
Mcp(feat): Per-op + plan tools and a stdio server
tony Jun 23, 2026
12751dc
Mcp(feat): Port mcp_swap config-swap dev script
tony Jun 23, 2026
e634210
Tests(chore): Run the mcp adapter suite in the gate
tony Jun 23, 2026
60bd985
Workspace(test): Cover analyzer, compiler, runner
tony Jun 23, 2026
4c83313
Mcp(feat): Add grok + agy CLIs to mcp_swap
tony Jun 23, 2026
21eb3b3
Mcp(feat): Caller-aware async tmux tool surface
tony Jun 23, 2026
0d87e33
Mcp(feat): Caller discovery + self-kill guards
tony Jun 23, 2026
a3eb234
Mcp(fix): Harden self-kill guards + socket scoping
tony Jun 23, 2026
8792574
Mcp(feat): Needle-free pane-output monitor
tony Jun 23, 2026
3e9a9cf
Mcp(feat): Make wait_for_output discoverable
tony Jun 24, 2026
8e7fac7
Mcp(fix): Close self-kill guard deferrals
tony Jun 24, 2026
79f5b39
Mcp(fix): Harden wait_for_output monitor
tony Jun 24, 2026
1f2048c
Workspace(feat): Thread env/shell/options through declarative tier
tony Jun 27, 2026
d606bf6
Workspace(feat): Add per-command Command (enter + sleeps)
tony Jun 27, 2026
afe253e
Ops(feat): Add ForwardCaptureError + ShowOptionsResult.get_int
tony Jun 27, 2026
f5d8290
Workspace(feat): Add Workspace.to_dict + read suppress_history
tony Jun 27, 2026
dd77912
Workspace(feat): Add BuildEvent stream + on_event observer
tony Jun 27, 2026
efebd5d
Workspace(feat): Add opt-in wait_pane readiness (anti-race)
tony Jun 27, 2026
860bae5
Workspace(feat): Honor explicit Window.window_index placement
tony Jun 27, 2026
bcb34dc
Mcp(feat): Expose build_workspace on the async server
tony Jun 27, 2026
731e613
Mcp(feat[safety]): Add tier constants, resolver, ExpectedToolError
tony Jun 27, 2026
faea0f0
Mcp(feat[middleware]): Port error-result + tail-preserving limiter
tony Jun 27, 2026
f8423fc
Mcp(feat[middleware]): Port safety, audit, readonly-retry middleware
tony Jun 27, 2026
f7c9946
Mcp(feat[safety]): Wire safety gate + middleware into the builders
tony Jun 27, 2026
f3bdf23
Mcp(feat[prompts]): Add recipe prompts in engine-ops vocabulary
tony Jun 27, 2026
1a9e2c7
Mcp(feat[resources]): Add tmux:// hierarchy resources over the engine
tony Jun 27, 2026
1206c03
Mcp(feat[lifespan]): Add engine-probe lifespan (async server)
tony Jun 27, 2026
32dd9f8
Workspace(feat[cli]): Add `load` command for .tmuxp.yaml files
tony Jun 27, 2026
53f6ec9
Workspace(feat): blank/pane empty-pane parity + cli --dry-run
tony Jun 27, 2026
c565cf1
Workspace(fix): First window's start_directory for its first pane
tony Jun 27, 2026
3f733b1
Ops(feat): Per-step host hook and bounded planner
tony Jun 27, 2026
7746e6a
Workspace(feat): Fold build dispatches by default
tony Jun 27, 2026
e66ad29
Workspace(feat): Fold --dry-run output
tony Jun 27, 2026
4dbca50
Ops(feat[new_pane]): Add floating pane operation
tony Jun 27, 2026
987daec
Ops(fix[break_pane]): Work around tmux 3.7 break-pane crash
tony Jun 27, 2026
7d68aef
Engines(feat): Resolve engine tmux version for gating
tony Jun 27, 2026
620d173
Workspace(feat[ir]): Add floating-pane declarations
tony Jun 27, 2026
a1fdacb
Workspace(feat[compiler]): Build floating panes from specs
tony Jun 27, 2026
683bb7c
Workspace(feat[compiler]): Cross-window floats via symbol table
tony Jun 27, 2026
1149789
Query(feat): Add snapshot-backed live pane query
tony Jun 27, 2026
6129c38
Query(feat): Add per-pane command building that folds
tony Jun 27, 2026
3fd0c04
Facade(feat[pane]): Add new_pane floating parity
tony Jun 27, 2026
e4e2b45
Mcp(feat[pane]): Add curated new_pane floating tool
tony Jun 27, 2026
eb60a2b
Mcp(feat[registry]): Surface whole-op min_version in schema
tony Jun 27, 2026
a7e2688
Mcp(fix[prompts]): wait_for_output uses target=
tony Jun 28, 2026
ac4886d
Workspace(fix[analyze]): Reject bad command items
tony Jun 28, 2026
689c1f1
Workspace(fix[env]): Inherit window env in splits
tony Jun 28, 2026
a988b2b
docs(CHANGES) Note on updates
tony Jul 4, 2026
617de95
Mcp(refactor): Drop dead is_conservative_caller
tony Jul 4, 2026
1a569f5
Engines(fix): Report tmux version over control mode
tony Jul 4, 2026
c0758ab
Ops(docs): Fix stale fold comment in chain test
tony Jul 4, 2026
312a5f3
Models(feat): Add PaneSnapshot.floating flag
tony Jul 4, 2026
232b15b
Engines(fix): Reap control-mode phantom sessions
tony Jul 4, 2026
c4e70dc
Engines(fix): Close subscribers on engine death
tony Jul 4, 2026
cba3a33
Query(feat): Split-type forward-ref pane handles
tony Jul 4, 2026
f097df7
Fluent(feat): Add plan() forward-ref build tier
tony Jul 4, 2026
fac08d5
Workspace(feat): Add freeze (live server to IR)
tony Jul 4, 2026
12bf2a1
Workspace(feat): Add variant expand + workspace sets
tony Jul 4, 2026
57a6c90
Ops(feat): Add plan explain() and astream()
tony Jul 4, 2026
b30c092
Fluent(feat): Add sleep/wait host boundaries
tony Jul 4, 2026
f4f7d9d
Mcp(feat): Add explain_plan tool
tony Jul 4, 2026
c850900
Ops(feat): Add conditional find-or-create via ensure()
tony Jul 4, 2026
4ec66ee
Fluent(feat): Add find_or_create_session
tony Jul 4, 2026
18e457f
Test(fix): Clean up sessions in fluent live tests
tony Jul 4, 2026
9f4b5ed
docs(experimental): Document the fluent plan() builder
tony Jul 4, 2026
f924715
Mcp(fix): Preserve ensure across plan-tool serialization
tony Jul 4, 2026
29a076d
Ops(test): Make the ensure probe test format-honest
tony Jul 4, 2026
cb5aa84
Workspace(docs): on_event and astream caveats
tony Jul 4, 2026
d648077
Workspace(fix): Stringify option/env values
tony Jul 5, 2026
18c8506
Ops(fix): Skip marked fold for detached creators
tony Jul 5, 2026
bf897fd
Engines(fix): End subscribe() after engine death
tony Jul 5, 2026
f279ccb
Query(refactor): Namespace dataclasses.replace
tony Jul 5, 2026
bc159d3
docs(experimental): Drop prototype lineage
tony Jul 5, 2026
e80239b
Workspace(fix): Honor window_shell on window 0
tony Jul 5, 2026
800c8c7
Workspace(fix): wait_pane uses effective shell
tony Jul 5, 2026
cf1535b
docs(experimental): Add arun/aexecute doctests
tony Jul 5, 2026
7f6ed38
Facade(fix): Add AsyncWindow.select_layout
tony Jul 5, 2026
5df981d
Engines(fix): ImsgEngine reports tmux version
tony Jul 5, 2026
83d305f
Mcp(fix): Gate wait_for_output prompts on events
tony Jul 5, 2026
942cbbe
Mcp(fix): Redact non-str sensitive audit args
tony Jul 5, 2026
96ba314
docs(experimental): Trim more branch narrative
tony Jul 5, 2026
155736d
Mcp(docs): Fix build_workspace registration claim
tony Jul 5, 2026
bad3ae5
Workspace(fix): Confirm cwd on the first pane
tony Jul 5, 2026
2f7bce9
Engines(feat[async_control_mode]): Add supervised reconnect
tony Jul 5, 2026
17e2e1e
Mcp(fix[events]): Restart the event drain after reconnect
tony Jul 5, 2026
afd3e2f
Objects(refactor): Rename facade package to objects
tony Jul 5, 2026
d813523
Engines(fix[async_control_mode]): Escalate backoff on connect-then-die
tony Jul 5, 2026
51a1224
Engines(fix[async_control_mode]): Terminate the prior proc on reconnect
tony Jul 5, 2026
34f1caf
Scripts(feat[bench]): Add hermetic engine build-benchmark grid
tony Jul 5, 2026
27f92a0
Scripts(docs[bench]): Add engine benchmark results
tony Jul 5, 2026
f3083ff
Scripts(fix[bench]): Make bench_engines mypy-clean under `mypy .`
tony Jul 5, 2026
b305ee3
Engines(refactor[mock]): Rename ConcreteEngine to MockEngine
tony Jul 5, 2026
e88e87b
chore(deps): Sync lock with master's gp-sphinx 0.0.1a33
tony Jul 5, 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
Loading
Loading