| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Sorry, something went wrong.
Feedback from driving tmux through an MCP layer (libtmux-mcp)Some real-world motivation for this exploration, from the perspective of an automation/agent layer that drives tmux through an MCP server built on libtmux. The pain todayBuilding a layout programmatically — say a right-hand column of four evenly stacked panes — currently takes four ordered tool calls, each splitting the pane id returned by the previous one: split %1 right -> %2 split %2 below -> %3 split %3 below -> %4 split %4 below -> %5 Each call is a separate libtmux dispatch → separate tmux process → separate command queue. The new pane id doesn't exist until the call returns, so I can't express the whole layout up front; I'm forced into a dependency chain of round-trips, one per pane. On the CLI the same thing is a single invocation: tmux split-window -h \; split-window \; split-window \; split-windowThat works because every command in one invocation shares tmux's current target state, and split-window advances it to the newly created pane — unless -d is set: https://github.com/tmux/tmux/blob/3f651d9/cmd-split-window.c#L176-L177 libtmux's split(attach=False) always passes -d, so that cursor never advances, and because each MCP call is its own process the shared queue state is gone at the boundary anyway. Net effect: there's no way to fold a layout build into one atomic tmux invocation today. The pattern that would be the unlockExactly what this PR is prototyping: an ordered sequence of typed commands that compiles to one native \; invocation, with targets that can be deferred and resolved rather than known up front. The substrate is already the right shape — CommandSequence.argv() semicolon-joins the calls and .run() dispatches through a single runner.cmd(...): https://github.com/tmux-python/libtmux/blob/228ff0b/tests/chainable_commands_experiment/shared.py Collapsing N round-trips into one invocation is the headline win; recovering tmux's own cursor semantics inside that single queue is the bonus. Deferred plandeferred_plan_api is the variant that maps most directly onto what an automation layer needs:
The big deal: I never thread freshly created ids through arguments. I query live state, map each row to commands, and let the plan resolve and dispatch atomically. The companion auto_batch_api is worth keeping alongside it — it encodes the detection rule for "what may fold into a chain": a call is chain-safe iff its output isn't consumed mid-chain. DeferredCommandResult raises if you read stdout before dispatch, and show_option is the deliberate counterexample of a call that can't batch because it needs output immediately: Combined with the CommandSpec.chainable flag, that's both the static and the dynamic half of deciding what a batch compiler may merge. What an async deferred plan adds on topThe async facade is more than a coat of paint for anything hosting this inside a server:
So: the sync deferred plan removes the round-trip and id-threading problem; the async deferred plan makes that same guarantee safe to use under a concurrent server without giving up the one-invocation-per-plan property. One prerequisite worth calling out for the split-chain case specifically: an attach opt-out (so a sequence can intentionally let the current cursor walk into freshly created panes) interacts with the -d behavior linked above. The snapshot/deferred-target approach sidesteps needing that for target resolution, which is part of why it's the more attractive path. |
Sorry, something went wrong.
Review: the pattern this lab should converge onI went through the whole tests/chainable_commands_experiment/ lab — every prototype and its tests — against five axes: developer experience, typing, testability, maintainability, and expressiveness. This writes up the tradeoffs of all of them and names the single pattern they point at, building on the libtmux-mcp motivation already in this thread. The pattern this points at
Concretely that is deferred_plan_api sitting on the shared.py IR, with async_deferred_plan as the hosted-server front door and the chainability rule from auto_batch_api deciding what merges. Everything else is either a piece of that pattern, a different ergonomic wrapper over the same IR, or a more ambitious shape that costs more than native tmux sequencing can repay. Why this is the unlock (the MCP case in this thread)The motivating pain is real: building a four-pane column today is four ordered round-trips, because each new pane id doesn't exist until the previous call returns — every call is its own tmux process with its own command queue. On the CLI it's one invocation (split-window -h \; split-window \; …) because the commands share tmux's current target state. Two facts make the one-invocation path the prize:
The idea doing the heavy lifting: typed targetsThe differentiator across the whole lab is the typed-target row in deferred_plan_api. Rows carry pane_id: PaneTarget, window_id: WindowTarget, session_id: SessionTarget, and expose .cmd / .window namespaces already bound to the right target — so select-layout can only bind a window and send-keys only a pane, and the mapper never touches a raw string. Every other prototype takes target: str | int | None positionally, which is easy to get wrong. Paired with pure-snapshot compilation (to_sequence(snapshot) is a pure function), the entire query → argv path is testable in memory with a fake snapshot — no tmux, sub-millisecond — with live tmux only at .run(). That is the strongest testability story here. What the async facade actually buysasync_deferred_plan is more than a coat of paint for anything hosting this in a server: the runner becomes async def cmd(...) and snapshot acquisition becomes awaitable, so dispatching a layout no longer blocks the event loop, snapshot reads overlap with other awaits, and independent plans (e.g. several windows) resolve and dispatch concurrently. Crucially it stays a thin facade over the sync engine — it reuses the sync to_sequence — so the one-native-sequence-per-plan guarantee is preserved. If the host is async (an MCP server is), this isn't optional polish; it's the front door. The chainability contract (what auto_batch_api is really for)The valuable thing in auto_batch_api isn't the transparent-proxy ergonomics — it's the rule it encodes: a call may fold into a chain iff its output isn't consumed mid-chain. The dynamic half is demonstrated by DeferredCommandResult, whose .stdout/.stderr/.returncode raise DeferredOutputUnavailable until the chain runs, with show_option as the deliberate counterexample — a command that can't batch because it needs output immediately. Worth stating precisely: the static half — a CommandSpec.chainable flag — exists in the IR but is not yet wired to that dynamic predicate; auto_batch_api doesn't reference it. Connecting the two (a static chainable declaration plus the runtime "output-not-consumed" guard) is exactly the contract a batch compiler needs to decide what may merge. That's the part to carry forward, not the proxy UX. The gap that sets the scope lineThe hard limit is structured per-command output (capture-pane, show-option). One semicolon invocation returns one merged result — tmux gives no per-command demultiplexing. The lab confronts this honestly but doesn't solve it (CapturePane constructs but run() is flat; deferred_plan rejects data-returning mappers; auto_batch raises). The clean resolution is to keep the plan/sequence layer mutation-only: the ORM already does single-command read-back well (split-window -P -F '#{pane_id}'), and per-command demux inside a sequence would need fragile sentinel injection and drift toward transaction semantics this work explicitly treats as a non-goal. Tradeoffs of every prototype
Breakdown by groupThe core (detailed above). shared.py + deferred_plan_api + async_deferred_plan + the chainability predicate from auto_batch_api are the pattern; see the four sections above. Everything below is measured against them. Hand-authoring façades over the IR — command_object_api, context_api, builder_api, decorator_api. All four let you author a fixed sequence without a query; they differ only in ergonomics. command_object_api gives the most precise types (concrete frozen dataclasses, assert_type) and the best test story (pure argv plus live stop-on-error), but the most boilerplate — each command is a dataclass and a forwarding namespace method, and its output is still just a CommandCall, so it's largely redundant with composing the IR directly (CommandCall(...) >> CommandCall(...)). context_api and builder_api are smaller and keep construction cleanly separate from dispatch, but still cost one typed method per command — cheap to start, not cheap to scale. decorator_api has the nicest call site (reads like a normal typed function) but attaches metadata at runtime where static tools can't see it. Net: adopt at most one, as a thin façade over command values, and only once callers want hand-authoring beyond raw >>. Metadata placement — descriptor_api. This is the issue's nominal Django-@admin.display framing and the only demo that hangs metadata on a still-callable attribute. The cost is the per-command @overload __get__ machinery, which is heavy and doctest-expensive. The lighter alternative already in the lab is the CommandSpec ClassVar on command values, which carries the same metadata with no extra machinery. Reach for descriptors only if attribute-level discovery (e.g. downstream tool codegen) is proven necessary. Read / query surfaces — queryset_api, async_query_api, selection_api, expression_api, document_query_api, statement_api. A cluster exploring the read side: a familiar ORM filter/order_by/limit, awaitable reads, traversal (server().sessions().windows().panes().fields(...)), Q-object predicates, and metadata/statement filters. They're useful for typed reads, but they don't sequence mutations and their typing varies (selection/statement lean stringly-typed). They complement the snapshot the plan layer already reads rather than competing with the command core — and the plan's own map() over a snapshot covers the common "read into typed rows" need. Over-ambitious shapes (research only) — lazy_plan_api, dag_api, runnable_api, orchestration_api, ast_api, generated_client_api. These impose graph (dag; lazy_plan.optimize() is a no-op), concurrency/workflow (runnable's stream/pipeline, orchestration's Prefect-style task.map), source-introspection (ast via inspect.getsource — can't handle lambdas or REPL-defined functions), or codegen (generated_client, contingent on a generator that isn't here) vocabularies. A single ordered semicolon dispatch has no dependencies, concurrency, or streaming to model, so these oversell what tmux provides. Keep them as probes; don't ship. A maintainability lever worth surfacingEvery module in the lab currently carries zero doctests and passes CI only by living under tests/. The test config enables --doctest-docutils-modules and includes src/libtmux in testpaths, so the moment any shape moves into src/, the project's "every public method has a working doctest, no skips" rule applies to every method. Doctest burden scales with public surface area — the strongest practical argument for leading with the bounded-surface query-plan layer and resisting a wide command catalog (and its duplicated namespace methods) before callers need it. NetCarry forward shared.py as the IR, deferred_plan_api as the authoring surface, async_deferred_plan as the async front door, and the chainability predicate from auto_batch_api (wired to a static CommandSpec.chainable flag) as the merge-safety contract — plus an attach/-d opt-out for the create-within-one-invocation layout case. Keep AST, DAG, orchestration, streaming, lazy-plan, and the standalone command-object/descriptor surfaces as research until a caller proves each one earns its surface. |
Sorry, something went wrong.
why: Promote the converged chainable-commands design from the PR #684 research into a documented, typed experimental API, beginning with the argv IR substrate. Establishes the _experimental package as a home for in-progress designs (mirroring _internal/docs/internals), explicitly outside the versioning policy. what: - Add src/libtmux/_experimental/ + chainable_commands subpackage - Add ir.py: CommandCall, CommandSequence (argv/argvs/then/>>/run), CommandSpec, CommandRunner/CommandResultLike protocols, and ;-escaping, all with doctests - Add tests/_experimental/chainable_commands/test_ir.py (pure argv + live tmux one-dispatch and stop-on-error) - Add docs/experiment/ landing + IR autodoc page; wire into docs/index.md toctree; mark _experimental not-public in public-api.md
why: Promote the converged chainable-commands design from the PR #684 research into a documented, typed experimental API, beginning with the argv IR substrate. Establishes the _experimental package as a home for in-progress designs (mirroring _internal/docs/internals), explicitly outside the versioning policy. what: - Add src/libtmux/_experimental/ + chainable_commands subpackage - Add ir.py: CommandCall, CommandChain (argv/argvs/then/>>/run), CommandSpec, CommandRunner/CommandResultLike protocols, and ;-escaping, all with doctests - Add tests/_experimental/chainable_commands/test_ir.py (pure argv + live tmux one-dispatch and stop-on-error) - Add docs/experiment/ landing + IR autodoc page; wire into docs/index.md toctree; mark _experimental not-public in public-api.md
why: Promote the converged chainable-commands design from the PR #684 research into a documented, typed experimental API, beginning with the argv IR substrate. Establishes the _experimental package as a home for in-progress designs (mirroring _internal/docs/internals), explicitly outside the versioning policy. what: - Add src/libtmux/_experimental/ + chainable_commands subpackage - Add ir.py: CommandCall, CommandChain (argv/argvs/then/>>/run), CommandSpec, CommandRunner/CommandResultLike protocols, and ;-escaping, all with doctests - Add tests/_experimental/chainable_commands/test_ir.py (pure argv + live tmux one-dispatch and stop-on-error) - Add docs/experiment/ landing + IR autodoc page; wire into docs/index.md toctree; mark _experimental not-public in public-api.md
why: Promote the converged chainable-commands design from the PR #684 research into a documented, typed experimental API, beginning with the argv IR substrate. Establishes the _experimental package as a home for in-progress designs (mirroring _internal/docs/internals), explicitly outside the versioning policy. what: - Add src/libtmux/_experimental/ + chainable_commands subpackage - Add ir.py: CommandCall, CommandChain (argv/argvs/then/>>/run), CommandSpec, CommandRunner/CommandResultLike protocols, and ;-escaping, all with doctests - Add tests/_experimental/chainable_commands/test_ir.py (pure argv + live tmux one-dispatch and stop-on-error) - Add docs/experiment/ landing + IR autodoc page; wire into docs/index.md toctree; mark _experimental not-public in public-api.md
why: Promote the converged chainable-commands design from the PR #684 research into a documented, typed experimental API, beginning with the argv IR substrate. Establishes the _experimental package as a home for in-progress designs (mirroring _internal/docs/internals), explicitly outside the versioning policy. what: - Add src/libtmux/_experimental/ + chainable_commands subpackage - Add ir.py: CommandCall, CommandChain (argv/argvs/then/>>/run), CommandSpec, CommandRunner/CommandResultLike protocols, and ;-escaping, all with doctests - Add tests/_experimental/chainable_commands/test_ir.py (pure argv + live tmux one-dispatch and stop-on-error) - Add docs/experiment/ landing + IR autodoc page; wire into docs/index.md toctree; mark _experimental not-public in public-api.md
why: Promote the converged chainable-commands design from the PR #684 research into a documented, typed experimental API, beginning with the argv IR substrate. Establishes the _experimental package as a home for in-progress designs (mirroring _internal/docs/internals), explicitly outside the versioning policy. what: - Add src/libtmux/_experimental/ + chainable_commands subpackage - Add ir.py: CommandCall, CommandChain (argv/argvs/then/>>/run), CommandSpec, CommandRunner/CommandResultLike protocols, and ;-escaping, all with doctests - Add tests/_experimental/chainable_commands/test_ir.py (pure argv + live tmux one-dispatch and stop-on-error) - Add docs/experiment/ landing + IR autodoc page; wire into docs/index.md toctree; mark _experimental not-public in public-api.md
why: Promote the converged chainable-commands design from the PR #684 research into a documented, typed experimental API, beginning with the argv IR substrate. Establishes the _experimental package as a home for in-progress designs (mirroring _internal/docs/internals), explicitly outside the versioning policy. what: - Add src/libtmux/_experimental/ + chainable_commands subpackage - Add ir.py: CommandCall, CommandChain (argv/argvs/then/>>/run), CommandSpec, CommandRunner/CommandResultLike protocols, and ;-escaping, all with doctests - Add tests/_experimental/chainable_commands/test_ir.py (pure argv + live tmux one-dispatch and stop-on-error) - Add docs/experiment/ landing + IR autodoc page; wire into docs/index.md toctree; mark _experimental not-public in public-api.md
why: Compare native tmux command sequencing behind typed prototypes without changing libtmux public APIs. what: - Add typed CommandInvocation and CommandChain primitives - Render tmux command sequences with semicolon separators and escaped literal semicolons - Cover single-dispatch runner behavior and native tmux queue semantics
why: Test a Django admin display-style command contract while preserving normal function signatures for completion. what: - Add tmux_command decorator with typed CommandSpec metadata - Add typed new-window and split-window factories - Cover metadata recovery and command-chain composition
why: Evaluate command descriptors as a statically visible alternative to runtime-only command metadata. what: - Add bound command objects exposed through descriptors - Preserve concrete call signatures for new-window and split-window - Cover descriptor binding, metadata, and chain composition
why: Evaluate a query-builder style API with simple typed methods and immutable sequence composition. what: - Add CommandFactory methods for new-window and split-window - Add sequence helper for Q-like ordered expressions - Cover completion-friendly calls and chain rendering
why: Evaluate explicit recording as a way to batch command methods while keeping editor-visible method signatures. what: - Add CommandBatch context manager with typed command methods - Record invocations into an immutable CommandChain - Cover context recording and static return types
…otype why: Test whether self-returning command methods can be automatically batched without changing frontend ergonomics. what: - Add RecordingTarget that captures cmd-style invocations - Demonstrate self-returning method batching - Cover immediate-output access as an explicit auto-batching limitation
why: Evaluate whether AST validation can preserve completion while deriving command chains from a scripted callable. what: - Add typed AST proxy and conservative source-shape validation - Build command chains by executing validated proxy callables - Cover supported tuple calls and rejected control flow
…emos why: Compare pipeline-style and workflow-style APIs for typed tmux command sequencing. what: - Add a RunnableCommand demo with invoke, batch, stream, and composition - Add a Hamilton-style command DAG demo with dependency-ordered sequencing - Add a Prefect-style deferred task submission demo
why: Compare Django, Piccolo, and document-shaped query ergonomics for tmux metadata. what: - Add a QuerySet-style lazy pane query demo - Add an async pane query demo with all and first terminals - Add a document-query metadata demo without new dependencies
why: Compare immutable statement, lazy plan, typed expression, and nested-selection APIs. what: - Add a SQLAlchemy-style generative command statement demo - Add Polars-style and Ibis-style lazy expression demos - Add a GraphQL-style nested selection demo
…ent demo why: Show the ergonomics and tradeoffs of generated autocomplete-first command surfaces. what: - Add nested generated session and window command namespaces - Test generated calls against the shared CommandCall and CommandSequence IR
why: Show the command-as-value pattern with pure argv tests and explicit execution boundaries. what: - Add nested command classes such as PaneCmd.SplitWindow and SessionCmd.NewWindow - Add a command-object-aware sequence and recorder namespace demo - Cover argv rendering, fake-runner batching, live tmux sequencing, mypy, and ty-compatible typing
why: Prove deferred query-driven command construction with typed targets and pure snapshot tests. what: - Add typed target/ref/query/plan demo - Add snapshot compilation and one-dispatch run tests - Keep structured-output command handling out of scope for this pattern
…an demo why: Show how deferred query command plans can keep typed command construction while awaiting snapshot and dispatch boundaries. what: - Add async facade over the deferred command plan demo - Add tests for async sequence inspection, one-dispatch execution, data maps, and empty no-op plans - Reuse typed targets, refs, and command values from the sync deferred demo
why: Promote the converged chainable-commands design from the PR #684 research into a documented, typed experimental API, beginning with the argv IR substrate. Establishes the _experimental package as a home for in-progress designs (mirroring _internal/docs/internals), explicitly outside the versioning policy. what: - Add src/libtmux/_experimental/ + chainable_commands subpackage - Add ir.py: CommandCall, CommandChain (argv/argvs/then/>>/run), CommandSpec, CommandRunner/CommandResultLike protocols, and ;-escaping, all with doctests - Add tests/_experimental/chainable_commands/test_ir.py (pure argv + live tmux one-dispatch and stop-on-error) - Add docs/experiment/ landing + IR autodoc page; wire into docs/index.md toctree; mark _experimental not-public in public-api.md
| Back | FazBrowse Home | New Git URL |
Summary
Naming Outcome
The authoring APIs use batch/commands vocabulary rather than recorder vocabulary. Recorder reads like persisted history or event capture; these demos author future tmux command sequences.
Shared Substrate
The lowest-level contract is an immutable command call or sequence. It renders to one native tmux argv sequence with ; separator tokens, then dispatches once through a runner that looks like Server.cmd().
Tradeoff: this is a good internal IR, but too low-level to be the main user-facing API.
Decorator API
Django-admin-style command metadata can be attached to normal typed functions.
Tradeoff: excellent function-call ergonomics, but runtime-attached metadata needs helper protocols or stubs for static tools.
Descriptor API
Descriptors bind concrete command objects and expose metadata on command-like attributes.
Tradeoff: strong metadata placement and completion, with more per-command boilerplate.
Builder And Context APIs
Builder and batch-context APIs keep construction explicit and type-checker-friendly.
Tradeoff: very testable, but separate from existing object methods unless a future integration exposes it from Server, Session, Window, or Pane.
Auto-Batch And AST APIs
Transparent auto-batching can work for self-returning methods, but immediate-output methods need explicit deferred-result semantics.
The AST prototype keeps typed proxy calls and validates a restricted callable shape before executing it against a proxy that accumulates calls.
Tradeoff: both are useful research probes, but should stay research-only unless a later feature explicitly needs their constraints.
Runnable And Orchestration APIs
Runnable composition gives symmetric one-off, batch, stream, and pipeline-style execution.
Prefect-style task submission maps deferred work to a native sequence.
Tradeoff: useful vocabulary for deferred work, but pipeline/concurrency naming may oversell what native tmux sequencing does.
Query And Read APIs
QuerySet, async-query, lazy-plan, expression, selection, and document-query demos explore read/query surfaces.
Tradeoff: strong for metadata/query APIs, less direct for mutating command sequences.
Command Object API
The command-object demo makes each tmux command constructible, testable, composable, and runnable without dispatching immediately.
Command objects compose into native semicolon sequences and can be grouped through typed command namespaces.
Tradeoff: this has the most boilerplate, but it gives the strongest combination of completion, static typing, fast argv tests, and controlled integration tests.
Deferred Query Command Plan API
This demo combines typed targets, typed query rows, deferred each() command construction, pure snapshot compilation, and one-dispatch execution.
Rows are typed refs, not raw dictionaries, so row-bound command namespaces carry their target automatically.
each() is a side-effect command plan, while map() stays data-only and flat_map() is the explicit multi-command expansion API.
Tradeoff: this is the strongest shape so far for target-safe deferred sequencing. It intentionally does not solve structured output commands such as capture-pane; those need a separate result strategy: per-item execution, delimiter-based parsing, or a typed output mode.
Async Deferred Command Plan API
The asyncio demo keeps the same typed command-authoring surface as the sync deferred plan. Only tmux-state resolution and command dispatch become awaitable.
Data queries are also awaitable, but remain separate from command construction.
Tradeoff: this proves the sync command IR can support an async transport without making command construction itself async. It does add facade boilerplate, so it only seems worth adopting if libtmux grows a real async runner or adapter layer.
Recommendation
For a v1 internal design, explicit command objects plus deferred query command plans look strongest.
Command objects give the cleanest unit-test boundary: construct one command, assert argv(), compose it, and only run tmux in integration tests. Deferred query command plans add the missing target-safe ergonomic layer: resolve typed rows, bind commands from each row, inspect the native sequence with a fake snapshot, then execute once through tmux when safe. An async facade can reuse the same IR if libtmux needs awaitable snapshot and dispatch boundaries. Descriptors remain useful if command metadata should live on command-like attributes. AST and transparent auto-batching should remain research-only.
Test Plan
Refs: #683