Skip to content

Typed tmux operation chains#81

Open
tony wants to merge 15 commits into
mainfrom
chainable-commands-experiment-00
Open

Typed tmux operation chains#81
tony wants to merge 15 commits into
mainfrom
chainable-commands-experiment-00

Conversation

@tony

@tony tony commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace the raw run_command_chain and narrow build_forward_layout tools with one typed run_tmux_operations tool.
  • Add discriminated Pydantic operation models for split_pane, send_keys, resize_pane, select_layout, set_option, and capture_pane, plus structured per-step and per-dispatch results.
  • Compile ordered typed operations to the fewest safe native tmux dispatches: no-output mutations fold into tmux a ; b ; c, output/id capture steps stay attributable, and on_error="continue" uses standalone dispatches because native tmux chains abort on first failure.
  • Preserve the single-split decoration fast path: an id-producing split_pane can fold with immediate send_keys / resize_pane operations that target its pane_ref through tmux's {marked} target while still returning the concrete pane id.
  • Pin the libtmux dependency to a public commit instead of a sibling worktree path.

Changes by area

src/libtmux_mcp/tools/chain_tools.py

Adds the typed compiler and registers run_tmux_operations as a mutating, open-world tool. The compiler keeps a pending chain of no-output operations, flushes before output reads, captures split ids when needed, and returns both rendered argv and per-step status.

src/libtmux_mcp/models.py

Adds the typed operation union and structured result models. The operation list is validated through a module-level Pydantic TypeAdapter in the tool module.

Docs

Adds chain-tool docs and wires chain_tools plus the new models into the FastMCP docs catalog.

Dependency pin

Updates [tool.uv.sources] and uv.lock to use the public libtmux branch commit 591a312f78d165816bb95a035a46219657c4b53d.

Design notes

  • The tool is typed, not raw tmux argv. Unsupported commands such as kill-server are not operation variants.
  • Per-step stdout is only promised where it can be attributed. capture_pane and id-producing split steps force a dispatch boundary unless the single split-ref {marked} optimization applies.
  • on_error="continue" deliberately disables native chaining. tmux ; sequences have abort-on-first-error semantics, so continuing later operations requires separate dispatches.
  • The true one-connection, per-command-result path remains a future control-mode runner; this PR stays on the subprocess chain API.

Test plan

  • rm -rf docs/_build
  • uv run ruff check . --fix --show-fixes
  • uv run ruff format .
  • uv run mypy .
  • uv run py.test --reruns 0 -vvv (612 passed)
  • just build-docs

Companion PR

Depends on the libtmux experimental chain API in tmux-python/libtmux#685.

tony added 6 commits June 20, 2026 11:19
why: Build the experimental chain-command MCP tools against the in-progress
libtmux._experimental.chain API on the sibling libtmux worktree.
what:
- Add [tool.uv.sources] libtmux = { path = "../libtmux", editable = true }
- Relock against the local editable checkout
why: Agents needed to run several tmux commands as one native invocation
instead of one tool call per command.

what:
- Add run_command_chain: a list of {command, args, target} folded into one
  `tmux a ; b` dispatch via libtmux._experimental.chain (CommandChain.run,
  run off the event loop with asyncio.to_thread)
- Destructive tier; refuse kill-server; fail closed on an empty list/target
- Add ChainCommand / RunCommandChainResult models; register the tool
- Tests: one-dispatch effect, atomic abort, validation, kill-server denial
why: A single tmux `;` chain can't hand back the ids it creates (a fresh
id can't be substituted into the same invocation), so callers had no way
to split a pane and learn the new pane ids.

what:
- Add build_forward_layout: split a seed pane N ways and return each new
  pane id, resolved over the minimum dispatches via ForwardPlan and
  AsyncServerPlanRunner (off the event loop)
- Optional per-split shell / send_keys; mutating tier (reaches a shell)
- Add ForwardSplit / ForwardLayoutResult models; register the tool
- Tests: two splits capture distinct ids, single-split fold + send_keys
  lands, empty-list validation
why: FastMCP log capture can surface the same child logger event through
both direct and parent propagation paths, which made the level check brittle.

what:
- Assert the set of matching fastmcp.errors levels
- Keep warning/error demotion coverage for tool errors
why: CI cannot install a sibling worktree path, so the MCP branch needs a
public immutable libtmux source for the experimental chain API.

what:
- Replace the editable sibling path with the published libtmux commit
- Regenerate uv.lock with the Git source
why: Raw tmux command chains forced callers to choose between one native
dispatch and typed, per-operation results. A typed compiler fills that gap
while keeping output and continue-on-error semantics honest.

what:
- Replace raw chain/layout tools with run_tmux_operations
- Add discriminated operation models and structured step results
- Fold chainable runs and split standalone output/id captures
- Document the new chain tool surface
@codecov-commenter

codecov-commenter commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.32135% with 112 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.65%. Comparing base (188784b) to head (7903964).

Files with missing lines Patch % Lines
src/libtmux_mcp/tools/chain_tools.py 73.79% 65 Missing and 33 partials ⚠️
src/libtmux_mcp/models.py 85.71% 12 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #81      +/-   ##
==========================================
- Coverage   84.67%   83.65%   -1.03%     
==========================================
  Files          43       44       +1     
  Lines        3197     3670     +473     
  Branches      438      515      +77     
==========================================
+ Hits         2707     3070     +363     
- Misses        360      436      +76     
- Partials      130      164      +34     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

why: A split that immediately feeds typed decorations should keep the
one-dispatch behavior that tmux supports through the marked-pane target while
still returning the created pane id.

what:
- Detect immediate send_keys/resize operations targeting a fresh split ref
- Compile them through tmux's {marked} target in one sequence
- Assert the single-dispatch split-ref path in tests and docs
@tony tony changed the title Experimental: one-dispatch + forward-layout chain command tools Typed tmux operation chains Jun 20, 2026
tony added 8 commits June 20, 2026 15:26
why: The typed operation compiler is now part of this branch's public
surface, but the unreleased notes and tool page did not describe what
callers can rely on.

what:
- Add an unreleased changelog entry for run_tmux_operations
- Document dispatch boundaries and the generic batch-tool boundary
why: The MCP operation compiler was duplicating libtmux's chainability
and scope contract, which let the two surfaces drift as new operations
were added.

what:
- Validate lowered commands with libtmux chain metadata
- Report contract failures as operation-level compile failures
- Add an exhaustiveness assertion for typed operation lowering
- Cover contract drift with focused tests
why: Agents need to inspect the native tmux dispatches a typed
operation list would produce before mutating tmux.

what:
- Add dry_run to run_tmux_operations and result models
- Return planned step and dispatch results with nullable exit codes
- Use deterministic placeholders for dry-run split pane refs
- Document dry-run behavior and add regression tests
why: Planned dry-run steps should not stop later operations when the
compiler flushes a pending dispatch before an output step.

what:
- Treat planned dry-run steps as successful for control flow
- Reuse the same success predicate for final results
- Add a regression covering dry-run output-step continuation
why: Native tmux chains can block the MCP call when a dispatch stalls,
so callers need a typed failure instead of an unbounded await.

what:
- Add dispatch_timeout validation to run_tmux_operations
- Mark timed-out dispatches and included steps as failed
- Cover chain, standalone, and marked split timeout paths
- Document the timeout behavior and background worker caveat
why: The typed chain compiler has branch-local failure paths for refs,
pending flushes, and marked split chains that need explicit coverage.

what:
- Cover unknown pane_ref compile failures
- Cover compile errors after a failed pending dispatch
- Cover marked split failure skipping later operations
why: A typed operation list can create panes before a later step fails,
leaving partial layout state behind for callers that need all-or-nothing
behavior.

what:
- Add rollback_on_error to run_tmux_operations
- Kill created split-ref panes in reverse order on failure
- Report rolled_back_panes and rollback_errors in results
- Document rollback behavior and cover enabled and disabled cases
why: The MCP chain tools need the pushed libtmux chain
control-mode surface that preserves per-command results.

what:
- Update the libtmux git pin to 6fc3db63
- Refresh uv.lock for the new pinned revision
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants