Typed operations, engines, workspace, query & MCP (#689)#690
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #690 +/- ##
===========================================
+ Coverage 51.78% 75.23% +23.44%
===========================================
Files 25 225 +200
Lines 3638 13638 +10000
Branches 733 1784 +1051
===========================================
+ Hits 1884 10260 +8376
- Misses 1448 2690 +1242
- Partials 306 688 +382 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
why: Record the experimental operations/engines layer for the upcoming release so the unreleased section tracks what landed. what: - Add a "What's new" deliverable under the unreleased 0.59.x section for the experimental operations and engines layer (#690) - Defer the release lead paragraph until the version is cut
Code reviewFound 2 issues:
libtmux/src/libtmux/experimental/ops/plan.py Lines 81 to 97 in e115eaf
libtmux/src/libtmux/experimental/ops/_ops/save_buffer.py Lines 38 to 41 in e115eaf 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Code reviewFound 1 issue:
libtmux/src/libtmux/experimental/ops/plan.py Lines 214 to 219 in 2e0b112 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code |
1 similar comment
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code |
why: Record the experimental operations/engines layer for the upcoming release so the unreleased section tracks what landed. what: - Add a "What's new" deliverable under the unreleased 0.59.x section for the experimental operations and engines layer (#690) - Defer the release lead paragraph until the version is cut
why: Record the experimental operations/engines layer for the upcoming release so the unreleased section tracks what landed. what: - Add a "What's new" deliverable under the unreleased 0.59.x section for the experimental operations and engines layer (#690) - Defer the release lead paragraph until the version is cut
why: Record the experimental operations/engines layer for the upcoming release so the unreleased section tracks what landed. what: - Add a "What's new" deliverable under the unreleased 0.59.x section for the experimental operations and engines layer (#690) - Defer the release lead paragraph until the version is cut
why: Record the experimental operations/engines layer for the upcoming release so the unreleased section tracks what landed. what: - Add a "What's new" deliverable under the unreleased 0.59.x section for the experimental operations and engines layer (#690) - Defer the release lead paragraph until the version is cut
Code reviewNo issues found. Fresh review of the current HEAD ( 🤖 Generated with Claude Code |
why: Operationalizes the typed-operations/engines architecture
(issues 688, 689) with the pure substrate that was absent from every
prototype branch: an inert, statically-typed operation value that
renders tmux commands, carries its result type, and serializes without
a live tmux server. Engines stay transport-agnostic over it. None of
this touches or changes existing public APIs.
what:
- Add libtmux.experimental.{ops,engines} packages (experimental, not
under the versioning policy)
- ops: frozen Operation[ResultT] with class-level metadata as the
single source of truth; pure render() with declarative version gating
(LooseVersion); build_result() adapting raw output to typed results
- ops: typed Result base + raise_for_status() (CPython/requests
precedent), SplitWindowResult/CapturePaneResult payloads
- ops: closed Target sum (PaneId/WindowId/SessionId/ClientName/NameRef/
IndexRef/Special/SlotRef) with fail-closed validation
- ops: fail-closed OperationRegistry keyed by kind, with OpSpec views
and predicate listing; stdlib dict serialization with round-trips
- ops: four seed operations (split-window, capture-pane, send-keys,
select-layout) registered via @register
- engines: TmuxEngine/AsyncTmuxEngine protocols, CommandRequest/
CommandResult, EngineSpec; run()/arun() execute bridge sharing one
render/build path (sync vs await is the only divergence)
- tests: 111 pure, fixture-parametrizable unit tests + doctests, all
runnable without a tmux server
why: Proves the operation/result contract is transport-agnostic -- the same typed result whether produced by a real tmux subprocess or an in-memory simulator -- and provides the offline engine that lets ops doctests and tests run without a tmux server (issue 689 phases 2-3). what: - engines.subprocess: classic SubprocessEngine mirroring tmux_cmd (has-session stderr fold, backslashreplace, trailing-blank strip; tmux failure returned as data, only missing binary raises), with for_server() deriving -L/-S/-f/-2 flags from a live Server - engines.concrete: deterministic in-memory engine (fabricated pane/ window/session ids, canned capture lines) for tests and docs - engines.registry: name-keyed engine registry (register/create/ available), seeded with subprocess + concrete - tests/experimental/contract: engine-agnostic operation contract run offline via concrete, plus classic-vs-concrete parity against a real tmux server (same result type + argv, payload may differ)
why: Completes the sync/async-symmetric execution story plus the deferred-execution and documentation mechanisms from issue 689 (phase 5 + docs), still without touching any existing API. what: - engines.asyncio: real AsyncSubprocessEngine on create_subprocess_exec (terminates the child on cancellation; not a thread wrapper), mirroring the classic engine's output handling so it returns the same typed result - ops.plan: LazyPlan records operations without touching tmux and resolves SlotRef forward refs at execute time via a sans-I/O generator; sync execute() and async aexecute() share one resolution core (run vs await arun is the only divergence); whole-plan serialization round-trips - ops.catalog: registry-driven CatalogEntry list (scope, version gates, effects, safety, result type, summary) -- the single source a docs domain renders, so runtime and docs cannot drift - tests: lazy resolution sync+async, plan serialization, catalog coverage, async-vs-sync classic parity against a real tmux server
why: Proves control mode is just another engine returning the same typed result (issue 689 phase 4) -- an operation run over a persistent tmux -C connection is indistinguishable, at the result level, from one run via fork-per-call subprocess. what: - engines.control_mode: ControlModeEngine over one persistent tmux -C connection; run_batch pipelines commands and parses each command's %begin/%end/%error block into a CommandResult; selectors-based nonblocking reads with timeout; startup-ACK discard; lifecycle via close()/context manager (lock-guarded teardown) - engines.control_mode: I/O-free ControlModeParser, unit-testable without tmux, adapted from the chain runner + protocol-engines parser - register control_mode in the engine registry and export it - tests: pure parser tests + real-tmux contract (split creates a real pane, batched commands, control-vs-concrete parity)
why: Demonstrates the "mode lives in the type" model from issue 689 -- EagerPane.split() returns a live EagerPane while LazyPane.split() returns a deferred LazyPane, each a single statically-known return type, both backed by the same SplitWindow operation. One Pane class with a runtime-bound engine could not type these return values distinctly. what: - facade.pane.EagerPane: executes immediately, returns live handles (split -> EagerPane), typed results for capture/send_keys - facade.pane.LazyPane: records into a LazyPlan, returns deferred handles (split -> LazyPane bound to the new pane's SlotRef), chainable - seed of the wider Server/Session/Window/Pane/Client x mode matrix - tests: eager live handles, lazy deferral + forward-ref resolution, and same-operation-backs-both-facades parity
why: Closes the two async gaps from issue 689: control mode and concrete had no async sibling. The async control engine is the one async engine that earns its place -- it adds an event stream subprocess cannot -- and prior libtmux/mux control-mode work (surfaced across agent histories via agentgrep, plus the asyncio-2 branches) shaped its correlation design. what: - engines.async_control_mode: AsyncControlModeEngine over a persistent tmux -C (create_subprocess_exec + one reader task). FIFO future correlation with skip-when-empty so unsolicited %begin blocks (hook- triggered commands and the startup ACK) never desync results; the startup ACK is consumed synchronously in start() to close the correlation race our whole-block parser would otherwise have. DEAD state fails pending commands on reader EOF/error. Cancellation via asyncio.wait_for (3.10 floor: no asyncio.timeout/TaskGroup). Bounded subscribe() notification stream with drop-counting. for_server() helper - engines.control_mode: ControlModeParser now surfaces bare %-notification lines via notifications() (additive; the sync engine ignores them) - engines.concrete: AsyncConcreteEngine sibling over shared simulation; removes the async test shim - ControlNotification typed event value - tests: parser notification/drain; async control vs real tmux (split, pipelined batch, concrete parity, live event stream, lifecycle)
why: the fluent forward-ref build tier had only method/module doctests; the experimental page covered the Core ops/engines/plans but not the declarative surface a user reaches for first. what: - Add a "Building fluently with plan()" section to the experimental page, proportional to the other sections: forward-ref handles, fold-unless- true-blocker, and find_or_create_session, with runnable ConcreteEngine doctests
why: the MCP plan tools rebuilt a serialized plan with add() + operation_from_dict, which drops the `ensure` probe that to_list emits -- so a find-or-create plan round-tripped through MCP silently became an unconditional create (a duplicate-session footgun). what: - Route _plan_from_dicts through LazyPlan.from_list, which carries the ensure probe, so a conditional create survives the MCP round-trip
why: the fake engine returned all three ids for any display-message, so
the ensure test asserted a pane binding while probing only #{session_id}
-- it passed on ids the probe never requested and would not catch a real
probe/capture format mismatch.
what:
- Make _FindEngine format-aware (return only the ids the probe requests)
- Probe the full capture format in test_ensure_probes_then_creates
- Add test_ensure_probe_must_match_create_capture: a session-only probe
binds no pane subref, guarding the format contract
why: The engine-ops streaming surface had two doc gaps a reader could trip on: abuild's on_event docstring invited a slow async sink (a fastmcp Context push) that would head-of-line-block the fold, and astream reads like a drop-in build though it skips host steps and can race send-keys against an unready shell. what: - runner/sets abuild(on_event=): note it is awaited inline on the dispatch coroutine (keep it fast and non-reentrant); a slow sink owns its own buffer and drains independently (the MCP _EventRing pattern); the Awaitable[None] type cannot enforce this - plan.astream: it observes dispatch only and skips host steps (sleep/before_script/wait_pane); use Workspace.abuild for a full build - comment the on_step closure so a future buffered mode uses bounded-queue backpressure, never drop -- BuildEvents carry unique tmux ids that must arrive exactly once
why: A window/session option or environment value typed as an int in YAML (e.g. `main-pane-height: 35`) reached the ;-chain renderer as an int and raised AttributeError in _escape_arg. The IR declares these Mapping[str, str] and tmux wants string args (classic libtmux str()s every arg), so analyze must coerce at ingest. what: - Add _str_map(): stringify the keys and values of an options or environment mapping - Apply it to all 7 ingest sites (session/window/pane options/options_after/global_options/environment) - Add parametrized coercion tests + a live folded-build regression
why: NewPane defaults to detached (-d), which does not focus the new
pane. The {marked} fold's untargeted `select-pane -m` then marks the
old active pane, so a NewPane >> SendKeys(slot) plan under the default
MarkedPlanner sent keys into the wrong pane. SplitWindow is unaffected
because it focuses its new pane.
what:
- _marked_decorates skips creators with detach=True; they dispatch
alone so their decorates bind the captured pane id via the slot
- Add a parametrized regression: split and focused NewPane still mark,
detached NewPane does not
why: subscribe() gated only on _closing, but a dead reader sets _dead, not _closing. A subscribe() after the engine died -- e.g. the output monitor or pull ring reconnecting once tmux closed stdout -- registered a fresh queue and blocked forever on queue.get(), the exact hang the close-subscribers work removed for the aclose() path. what: - Gate subscribe() on `_closing or _dead is not None` - Add a regression mirroring the after-close test but marking dead first
why: AGENTS.md's namespace-import rule exempts only dataclass/field from the dataclasses module; replace is a plain function and should be called as dataclasses.replace, matching the sibling snapshots.py. what: - import dataclasses; use dataclasses.replace in filter/order_by/limit
why: AGENTS.md's shipped-vs-branch rule keeps never-shipped, branch-internal lineage out of docstrings; the "chainable-commands" and "libtmux-protocol-engines" prototypes are sibling branches the reader never experienced. Describe only current state. what: - Trim prototype references in ops/plan.py, ops/_chain.py, engines/base.py, engines/registry.py
why: window_shell (a window's custom first-pane shell) rode new-window for windows 2..N, but window 0 is created by new-session, which had no shell parameter -- so the first window's first pane silently booted the default shell instead of the configured one. what: - Add window_shell to NewSession (a trailing shell-command, mirroring how NewWindow appends it for later windows) - compiler passes ws.windows[0].window_shell to NewSession - Test that window 0's window_shell rides new-session
why: The wait_pane readiness guard skipped the wait when a first pane set `shell=`, but the first pane's own shell is never applied (only window_shell reaches its creator). So the pane launched the default shell and send-keys raced the prompt -- the exact race wait_pane exists to prevent. what: - _emit_pane_commands takes the shell the creator actually applies instead of re-deriving pane.shell|window_shell: first pane -> window_shell, split -> pane.shell|window_shell, float -> pane.shell - Parametrized regression: a first-pane pane.shell still waits, a first-pane window_shell skips
why: AGENTS.md requires working doctests on all methods; the async twins arun (fluent) and aexecute (plan) had none, unlike their documented siblings run and astream. what: - Add runnable async doctests (asyncio.run over AsyncConcreteEngine)
why: EagerWindow and LazyWindow expose select_layout, but AsyncWindow lacked it -- `await window.select_layout(...)` raised AttributeError, breaking eager/lazy/async parity for the layout op. what: - Add AsyncWindow.select_layout mirroring the eager/lazy method - Assert it in the async facade parity test
why: ImsgEngine executes against a live tmux server but did not implement tmux_version(), unlike every other real-server engine. So resolve_engine_version() fell back to "assume latest", and a version-gated op run over imsg against an older server rendered a too-new flag that tmux rejects -- diverging from the subprocess and control-mode engines on the same server. what: - Add ImsgEngine.tmux_version(): memoized tmux -V via the resolved binary, mirroring SubprocessEngine - Test it satisfies SupportsTmuxVersion and reports/memoizes a version
why: The run_and_wait / diagnose_failing_pane / interrupt_gracefully prompts steer the agent to call wait_for_output, but that tool is registered only on a streaming control-mode async server. On the sync server (and non-streaming async) the agent was told to call a tool that does not exist -> tool-not-found. what: - register_prompts(events_enabled=): register the wait_for_output prompts only when events streaming is enabled; build_dev_workspace always registers - build_async_server passes its computed events_enabled; the sync build_server leaves it False - Parametrized gate regression + a sync-server integration check
why: The audit summary redacted a sensitive arg (keys/text/command/...) only when it was a str or dict; a non-conformant client sending e.g. command=["secret"] fell through to the raw-passthrough branch and logged the payload verbatim into the INFO audit record -- contradicting the documented "payload-bearing arguments never reach the log raw" promise. Schema validation rejects such calls, but _summarize_args runs first. what: - Shape-redact any other-typed sensitive value via _redacted_value_shape - Doctest that a list-valued sensitive arg is redacted
why: AGENTS.md shipped-vs-branch keeps never-shipped, branch-internal narrative out of docstrings; round 1 missed a few. what: - control_mode.py: drop the "chainable-commands runner / libtmux-protocol-engines parser" lineage sentence - concrete.py: drop "preserving the historical behaviour" (an intermediate in-branch state) - new_pane.py: drop the "first operation gated by min_version" ordinal; keep only the current-state VersionUnsupported clause
why: register_plan_tools' docstring said build_workspace is registered only on the synchronous server, but the async branch registers it too (dispatching to abuild_workspace) -- misleading a maintainer about tool exposure. what: - State it registers on both servers, per-engine dispatch
why: The session start_directory lands on window 0's first pane, but confirm() read the window's active_pane. When a non-first pane is focused (or a split leaves focus off pane 0) with a different cwd, confirm falsely reported "first pane cwd != declared". what: - Check the first window's first pane (index 0), not active_pane - Live regression: a focused non-first pane at a different cwd still confirms ok
why: A tmux restart or socket blip killed the async control-mode engine permanently -- the reader EOF'd, the engine went dead, and subscribers had to be torn down. A supervisor that self-heals keeps the event stream alive across the gap and lets subscribe() rely on _closing alone instead of a sticky _dead gate. what: - Replace the one-shot start()/reader with a supervisor that spawns tmux -C, replays desired subscriptions/attach, runs the reader inline (one at a time), and reconnects with deterministic jittered backoff when the reader returns on EOF. - Add add_subscription()/set_attach_targets() declarative state, replayed on every (re)connect; bump _generation per connect and reset parser/pending/attach before the new proc's bytes flow. - Surface first-connect failure to every start() caller; reset the backoff on a healthy connect. - Gate subscribe() on _closing only: a reconnecting (_dead) engine keeps its subscriber so the post-reconnect reader feeds it. - Cover supervisor reconnect, attach replay, and attach reset; drop the now-obsolete _dead-gate sentinel test.
why: The engine's supervisor now reconnects on a tmux restart or socket blip, but _broadcast_stream_end ends the subscribe stream on the disconnect, so the pull ring's drain task completes. A bare ``is None`` guard never re-subscribed, silently freezing the cursor after a reconnect. what: - Restart the drain in _EventRing._ensure_started when the prior task has completed (not just when unset). - Clear a prior drain error at the start of _drain (a fresh attempt) rather than on restart, so a still-unread failure is not wiped before since() surfaces it -- a persistently dead stream must not read as empty-but-healthy. - Cover restart-only-if-done and persistent-error surfacing with parametrized tests.
why: "Facade" is not idiomatic Python for these engine-typed classes. They are the domain-shaped tmux nouns -- server/session/window/pane/ client -- so the package reads as libtmux.experimental.objects, and an individual engine-bound class is a "wrapper" in prose. Retires the facade/handle vocabulary. what: - Rename src/libtmux/experimental/facade -> objects and its tests, keeping the Eager/Lazy/Async class names unchanged. - Reword package docstrings from "facade"/"handle" to "object". - Update the three stray package references (ops.results, ops.new_pane, docs/experimental) to "wrapper".
why: A proc that connects then instantly dies still consumes its startup ACK, so the supervisor counted it as a healthy connection and reset the reconnect backoff to zero every loop. A persistently flapping proc (fatal config, permission, or resource error) then fork-stormed tmux at ~10 Hz instead of backing off. what: - Reset the backoff only for a connection that survived a minimum lifetime; a connect-then-immediately-die counts as a failed attempt, so the backoff escalates. - Add _HEALTHY_CONNECTION_SECONDS and a regression test asserting a connect-then-die loop escalates instead of pinning at _backoff(0).
why: A reader that returned via an exception (not a clean EOF) leaves the tmux -C process alive. The supervisor then reconnected and _spawn overwrote _proc with the new process without terminating the old one, orphaning a control client on every such reconnect. what: - Terminate a still-alive prior proc at the top of _spawn before replacing it; a clean-EOF proc has already exited (no-op). - Add a regression test that _spawn terminates a live prior proc.
why: Quantify the experimental workspace builder's build cost across engines and answer "which engine, how fast" reproducibly, without ever touching the developer's live tmux server. what: - Add scripts/bench_engines.py, a self-contained PEP 723 script (uv run) that sweeps scenarios x engines x wait-modes and reports min/avg/median/p90/p95/p99/max as rich tables + JSON. - Engines: classic; the builder on subprocess/control_mode/imsg/ concrete; and a pipelined prototype that batches independent creates via run_batch. - Sandboxed: per-run isolated sockets under a throwaway dir, TMUX unset, atexit teardown + orphan backstop -- the default server is never contacted. - Subcommands: run (in-process grid), cell (one build, for hyperfine), profile (cProfile).
why: Version the measured build cost across engines (and the pipelining prototype) alongside the harness so the numbers are reproducible and reviewable. what: - Add scripts/bench-results/ with RESULTS.md (analysis: the engine grid, with/without shell-readiness wait, profile, and the ~79x reconciliation) plus grid.json / wait.json (raw per-run data from scripts/bench_engines.py).
why: The committed benchmark script failed a repo-wide `mypy .` sweep with 5 errors. Four stem from the `typer` PEP 723 inline dependency, which the repo's mypy environment cannot resolve (cascading into untyped-decorator errors); one is a genuine Server|None narrowing gap. The configured scope (files = [src, tests]) hides them, but a broad `mypy .` surfaces them. what: - Add a file-level disable-error-code for the two typer environment artifacts (import-not-found, untyped-decorator), keeping every other check strict. - Accept `Server | None` in ImsgForServer and assert non-None so the make_engine callable type narrows correctly.
why: "Concrete" named the in-memory simulator engine, but every real
engine (subprocess, control_mode, imsg) is equally a concrete
implementation. Across the Python/Rust ecosystem "Concrete*" is a
test-only convention for "a minimal instantiable ABC subclass", the
opposite of this docs/doctest workhorse, and the word already carries
its id/type sense throughout ops/query/objects. "Mock" names the engine
by its role: the no-tmux, in-memory stand-in.
what:
- Rename ConcreteEngine -> MockEngine and AsyncConcreteEngine ->
AsyncMockEngine; module concrete.py -> mock.py
- EngineKind.CONCRETE ("concrete") -> EngineKind.MOCK ("mock");
EngineSpec.concrete() -> EngineSpec.mock(); registry key becomes
"mock" (available_engines() re-sorts accordingly)
- Rename the benchmark engine key/label; update RESULTS.md and
grid.json to match
- Keep docstrings describing the in-memory simulation so the name's
test-double flavor does not mislead doctest readers
- Leave "concrete" untouched where it means a concrete id/target/pane
handle (ops, query, objects, workspace)
why: Rebasing onto master pulled in the gp-sphinx 0.0.1a33 docs-dep bump; the sibling sphinx-autodoc-* pins in uv.lock had to move to match or `uv lock --locked` fails. what: - Regenerate uv.lock: sphinx-autodoc-api-style and sphinx-autodoc-pytest-fixtures 0.0.1a32 -> 0.0.1a33
Summary
Implements the typed operations + engines architecture under
libtmux.experimental— an inert, statically-typed operation spine; a family of interchangeable engines (subprocess, concrete, control-mode, their async variants, and the native imsg easter-egg); lazy/async plans with;-folding chainability; pure object-graph snapshots; a typed read surface; engine-typed facades; a declarative tmuxp-style workspace builder; a live pane query DSL; tmux 3.7 floating panes; an optional Model Context Protocol server; and a docs catalog generated from the registry.Operationalizes #688 (architecture) per the plan in #689. Touches no existing public API — everything is additive under
libtmux.experimental(explicitly outside the versioning policy). Nothing is generated at runtime; everything is statically typed and checker-clean.What's delivered
The spine —
libtmux.experimental.ops(pure, no tmux):Operation[ResultT]: frozen, keyword-only, class-vars as the single source of truth (kind/command/scope/result_cls/effects/safety/chainable/version gates). Purerender()with declarative version gating;build_result()adapts raw output to a typed result (version-threaded so read parsing matches the gated render).Resulthierarchy with opt-inraise_for_status():AckResult,SplitWindowResult/CreateResult(captured ids),CapturePaneResult,ListPanes/Windows/Sessions/ClientsResult(snapshot-deriving rows), plusHasSessionResult,DisplayMessageResult,ShowOptionsResult,ShowBufferResult.Targetsum, fail-closedOperationRegistry, stdlib serialization, andcatalog()(registry-derived docs data).LazyPlan(record → resolveSlotRefforward refs → execute). Folding is planner-based: pass aPlanner— sequential (onetmuxcall per op),FoldingPlanner(;-chainedtmux a ; b), orMarkedPlanner({marked}-fold). All yield an identicalPlanResult; only the dispatch count differs, and failure attribution matches tmux'scmdq_remove_group(first failed, rest skipped).ListPanes/ListWindows/ListSessions/ListClientsrender the same-Ftemplate neo uses (imported, not copied) and parse intomodelssnapshots — a typed read surface parallel to neo, leaving the ORM untouched.Engines —
libtmux.experimental.engines(all behindTmuxEngine/AsyncTmuxEngine, all returning the sameCommandResult):SubprocessEngineAsyncSubprocessEngineConcreteEngineAsyncConcreteEnginetmux -C)ControlModeEngineAsyncControlModeEngine(event stream viasubscribe())ImsgEngine(opt-in easter egg)Control engines use an I/O-free bytes
ControlModeParserwith FIFO/skip correlation (startup-ACK consumed up front; unsolicited hook blocks skipped), report their tmux version for runtime gating, reap the throwaway session a baretmux -Cimplies, and end event subscribers cleanly on engine death. The imsg engine speaks tmux's binary peer protocol directly (AF_UNIX+SCM_RIGHTS,PROTOCOL_VERSION8) with a live parity test vs the subprocess engine.Models —
libtmux.experimental.models: frozenPane/Window/Session/Client/ServerSnapshot(typed core + raw field tail),from_pane_rows()builds the whole tree from onelist-panes -aquery, round-trips to plain dicts.Facades —
libtmux.experimental.facade("mode lives in the type"): the full eager / lazy / async × Server/Session/Window/Pane/Client matrix over the same ops; control mode is just an engine choice.Declarative workspace —
libtmux.experimental.workspace: a tmuxp-styleWorkspacedeclares a session as a tree of windows and panes and lowers to a CoreLazyPlan.build/abuildfold the dispatches by default (a multi-pane window collapses to a handful of;-chained +{marked}calls, identical result to an unfolded build); host steps (per-command sleeps, the opt-inwait_paneanti-race) stay hard fold boundaries. Threads env/shell/options through the IR, round-trips viato_dict, emits a build-event stream, supports floating panes (including cross-window overlays wired through agraphlibsymbol table), and ships aloadCLI for.tmuxp.yaml.Live pane query —
libtmux.experimental.query:panes()opens a lazy, chainable query over the panes a running server has (filter/order_by/limit/map, includingfilter(floating=True)), reading nothing until a terminal call.commands()records per-pane ops (send keys, resize, select, respawn, clear history, kill) into aLazyPlanthat folds to one tmux dispatch. Resolves against a live engine or a plain sequence of snapshots (offline in tests).Floating panes (tmux 3.7): available from the operation layer (a
new-paneop with absolute geometry and zoom), the pane facades (new_pane()), the MCP surface (a curated tool), and workspace specs (Workspacefloat declarations).MCP server —
libtmux.experimental.mcp(optionallibtmux[mcp]extra): a framework-agnostic tool projection (no hard MCP/pydantic dependency) plus a thin FastMCP adapter, launched aslibtmux-engine-mcp. Three tiers: a curated vocabulary of intuitive verbs (~40) that mirror the ORM, a per-operation tool for every operation (hidden by default), and plan tools that preview or build a whole workspace. It is caller-aware (discovers the launching pane and refuses to kill or respawn its own pane/window/session), gates mutating and destructive tools behind a tieredLIBTMUX_SAFETYlevel until opted in, adds middleware (audit logging, read-only retry, tail-preserving output limits), serves a needle-freewait_for_outputmonitor andtmux://resources, and ships recipe prompts.Docs: an in-repo
tmuxop-catalogSphinx directive renderscatalog()into the operation reference (exercised by the docs gate), so the reference can't drift from the code.Testing
ruff,ruff format,ty,pytest,build-docs.Design notes
raise_for_status(). Same result shape across engines.mcpextra, with no MCP/pydantic dependency in the core projection.attach(which falls back to a local spawn).Refs #688, #689.