From 8f8b8e6980a2b40d30e1d67963e3a18658d3bc7e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 16:29:35 -0500 Subject: [PATCH 01/19] fix(conf): Replace stale "master" branch references with "main" why: Repo default branch is main but 4 locations still referenced master, breaking Edit-on-GitHub links, redirect detection, and source code links. what: - docs/conf.py: source_branch, rediraffe_branch, linkcode_resolve URL - src/libtmux_mcp/__about__.py: __changes__ URL --- docs/conf.py | 6 +++--- src/libtmux_mcp/__about__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2b284c5..94df75e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -95,7 +95,7 @@ }, ], "source_repository": f"{about['__repository__']}/", - "source_branch": "master", + "source_branch": "main", "source_directory": "docs/", "announcement": "Pre-alpha. APIs may change. Feedback welcome.", } @@ -140,7 +140,7 @@ # sphinxext-rediraffe rediraffe_redirects = "redirects.txt" -rediraffe_branch = "master~1" +rediraffe_branch = "main~1" # sphinxext.opengraph ogp_site_url = about["__url__"] @@ -259,7 +259,7 @@ def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str: fn = relpath(fn, start=pathlib.Path(libtmux_mcp.__file__).parent) if "dev" in about["__version__"]: - return "{}/blob/master/{}/{}/{}{}".format( + return "{}/blob/main/{}/{}/{}{}".format( about["__repository__"], "src", about["__package_name__"], diff --git a/src/libtmux_mcp/__about__.py b/src/libtmux_mcp/__about__.py index 5bf5182..4ed7017 100644 --- a/src/libtmux_mcp/__about__.py +++ b/src/libtmux_mcp/__about__.py @@ -14,4 +14,4 @@ __url__ = "https://libtmux-mcp.git-pull.com" __bug_tracker__ = "https://github.com/tmux-python/libtmux-mcp/issues" __repository__ = "https://github.com/tmux-python/libtmux-mcp" -__changes__ = "https://github.com/tmux-python/libtmux-mcp/blob/master/CHANGES" +__changes__ = "https://github.com/tmux-python/libtmux-mcp/blob/main/CHANGES" From d1a07a8a918fd46dba97757312c39a17cac9b95a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 16:32:10 -0500 Subject: [PATCH 02/19] docs(tools): Add behavioral guidance to 22 tool docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: "Use when / Avoid when" guidance existed only in Markdown docs, invisible to agents at runtime. FastMCP exposes function __doc__ as MCP tool descriptions — enriching docstrings directly improves agent tool selection and workflow composition. what: - Add 2-3 line behavioral summary to each of 22 tool docstrings - Covers: use case, common next step, side effects, race conditions - Key addition: send_keys warns about capture_pane race condition - Destructive tools note self-kill protection and alternatives - Max ~100 words per tool to manage context window token cost - Existing docstring content (params, returns) unchanged --- src/libtmux_mcp/tools/env_tools.py | 5 +++++ src/libtmux_mcp/tools/option_tools.py | 6 ++++++ src/libtmux_mcp/tools/pane_tools.py | 17 +++++++++++++++++ src/libtmux_mcp/tools/server_tools.py | 13 +++++++++++++ src/libtmux_mcp/tools/session_tools.py | 9 +++++++++ src/libtmux_mcp/tools/window_tools.py | 15 +++++++++++++++ 6 files changed, 65 insertions(+) diff --git a/src/libtmux_mcp/tools/env_tools.py b/src/libtmux_mcp/tools/env_tools.py index 48221e5..04431e5 100644 --- a/src/libtmux_mcp/tools/env_tools.py +++ b/src/libtmux_mcp/tools/env_tools.py @@ -27,6 +27,8 @@ def show_environment( ) -> EnvironmentResult: """Show tmux environment variables. + Use to inspect tmux environment variables that affect child processes. + Parameters ---------- session_name : str, optional @@ -66,6 +68,9 @@ def set_environment( ) -> EnvironmentSetResult: """Set a tmux environment variable. + Use to set variables that will be inherited by new panes and windows. + Changes do not affect already-running processes. + Parameters ---------- name : str diff --git a/src/libtmux_mcp/tools/option_tools.py b/src/libtmux_mcp/tools/option_tools.py index 8dc23a8..acaf911 100644 --- a/src/libtmux_mcp/tools/option_tools.py +++ b/src/libtmux_mcp/tools/option_tools.py @@ -73,6 +73,9 @@ def show_option( ) -> OptionResult: """Show a tmux option value. + Use to check tmux configuration values such as history-limit, + mouse support, or status bar settings. + Parameters ---------- option : str @@ -109,6 +112,9 @@ def set_option( ) -> OptionSetResult: """Set a tmux option value. + Use to change tmux behavior at runtime. Common uses: adjusting + history-limit, enabling mouse support, changing status bar format. + Parameters ---------- option : str diff --git a/src/libtmux_mcp/tools/pane_tools.py b/src/libtmux_mcp/tools/pane_tools.py index ce79ab1..7fb3cc7 100644 --- a/src/libtmux_mcp/tools/pane_tools.py +++ b/src/libtmux_mcp/tools/pane_tools.py @@ -40,6 +40,10 @@ def send_keys( ) -> str: """Send keys (commands or text) to a tmux pane. + After sending, use wait_for_text to block until the command completes, + or capture_pane to read the result. Do not capture_pane immediately — + there is a race condition. + Parameters ---------- keys : str @@ -147,6 +151,8 @@ def resize_pane( ) -> PaneInfo: """Resize a tmux pane. + Use when adjusting layout for better readability or to fit content. + Parameters ---------- pane_id : str, optional @@ -205,6 +211,9 @@ def kill_pane( ) -> str: """Kill (close) a tmux pane. Requires exact pane_id (e.g. '%5'). + Use to clean up panes no longer needed. To remove an entire window + and all its panes, use kill_window instead. + Parameters ---------- pane_id : str @@ -245,6 +254,8 @@ def set_pane_title( ) -> PaneInfo: """Set the title of a tmux pane. + Use titles to label panes for later identification via list_panes or get_pane_info. + Parameters ---------- title : str @@ -287,6 +298,9 @@ def get_pane_info( ) -> PaneInfo: """Get detailed information about a tmux pane. + Use this for metadata (PID, path, dimensions) without reading terminal content. + To read what is displayed in the pane, use capture_pane instead. + Parameters ---------- pane_id : str, optional @@ -326,6 +340,9 @@ def clear_pane( ) -> str: """Clear the contents of a tmux pane. + Use before send_keys + capture_pane to get a clean capture without prior output. + Note: this is two tmux commands with a brief gap — not fully atomic. + Parameters ---------- pane_id : str, optional diff --git a/src/libtmux_mcp/tools/server_tools.py b/src/libtmux_mcp/tools/server_tools.py index e111247..51d2deb 100644 --- a/src/libtmux_mcp/tools/server_tools.py +++ b/src/libtmux_mcp/tools/server_tools.py @@ -31,6 +31,9 @@ def list_sessions( ) -> list[SessionInfo]: """List all tmux sessions. + Use as the starting point for discovery — call this before targeting + specific sessions, windows, or panes. + Parameters ---------- socket_name : str, optional @@ -61,6 +64,9 @@ def create_session( ) -> SessionInfo: """Create a new tmux session. + Check list_sessions first to avoid name conflicts. A new session + starts with one window and one pane. + Parameters ---------- session_name : str, optional @@ -105,6 +111,10 @@ def create_session( def kill_server(socket_name: str | None = None) -> str: """Kill the tmux server and all its sessions. + Destroys ALL sessions, windows, and panes on this server. Use kill_session + to remove a single session instead. Self-kill protection prevents killing + the server running this MCP process. + Parameters ---------- socket_name : str, optional @@ -134,6 +144,9 @@ def kill_server(socket_name: str | None = None) -> str: def get_server_info(socket_name: str | None = None) -> ServerInfo: """Get information about the tmux server. + Use to verify the tmux server is running before other operations. + For session-level details, use list_sessions instead. + Parameters ---------- socket_name : str, optional diff --git a/src/libtmux_mcp/tools/session_tools.py b/src/libtmux_mcp/tools/session_tools.py index 7d482b6..1fd1b30 100644 --- a/src/libtmux_mcp/tools/session_tools.py +++ b/src/libtmux_mcp/tools/session_tools.py @@ -81,6 +81,8 @@ def create_window( ) -> WindowInfo: """Create a new window in a tmux session. + Creates a window with one pane. Use split_window to add more panes afterward. + Parameters ---------- session_name : str, optional @@ -137,6 +139,9 @@ def rename_session( ) -> SessionInfo: """Rename a tmux session. + Use when a session's purpose has changed. Existing pane_id references + remain valid after renaming. + Parameters ---------- new_name : str @@ -167,6 +172,10 @@ def kill_session( ) -> str: """Kill a tmux session. + Destroys the session and all its windows and panes. Use kill_window + to remove a single window instead. Self-kill protection prevents + killing the session containing this MCP process. + Parameters ---------- session_name : str, optional diff --git a/src/libtmux_mcp/tools/window_tools.py b/src/libtmux_mcp/tools/window_tools.py index 7896a18..548a04a 100644 --- a/src/libtmux_mcp/tools/window_tools.py +++ b/src/libtmux_mcp/tools/window_tools.py @@ -111,6 +111,9 @@ def split_window( ) -> PaneInfo: """Split a tmux window to create a new pane. + Creates a new pane by splitting an existing one. Use direction to choose + above/below/left/right. Returns the new pane's info including its pane_id. + Parameters ---------- pane_id : str, optional @@ -188,6 +191,9 @@ def rename_window( ) -> WindowInfo: """Rename a tmux window. + Use when a window's purpose has changed. Existing window_id references + remain valid after renaming. + Parameters ---------- new_name : str @@ -227,6 +233,10 @@ def kill_window( ) -> str: """Kill (close) a tmux window. Requires exact window_id (e.g. '@3'). + Destroys the window and all its panes. Use kill_pane to remove a single + pane instead. Self-kill protection prevents killing the window containing + this MCP process. + Parameters ---------- window_id : str @@ -272,6 +282,9 @@ def select_layout( ) -> WindowInfo: """Set the layout of a tmux window. + Choose from: even-horizontal, even-vertical, main-horizontal, + main-vertical, or tiled. Rearranges all panes in the window. + Parameters ---------- layout : str @@ -319,6 +332,8 @@ def resize_window( ) -> WindowInfo: """Resize a tmux window. + Use to adjust the window dimensions. This affects all panes within the window. + Parameters ---------- window_id : str, optional From 88e80dad73b8efa8e063132420c920e3ae0bc41c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 16:32:53 -0500 Subject: [PATCH 03/19] docs(server): Add tier-hidden context to agent safety instructions why: When SafetyMiddleware blocks a tool, agents had no prior context about why. The instructions mentioned tier names but not the hiding behavior. what: - Add sentence to _build_instructions(): "Tools outside the active tier are hidden and will not appear in tool listings." --- src/libtmux_mcp/server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libtmux_mcp/server.py b/src/libtmux_mcp/server.py index 2562225..340b8b5 100644 --- a/src/libtmux_mcp/server.py +++ b/src/libtmux_mcp/server.py @@ -63,7 +63,9 @@ def _build_instructions(safety_level: str = TAG_MUTATING) -> str: "Available tiers: 'readonly' (read operations only), " "'mutating' (default, read + write + send_keys), " "'destructive' (all operations including kill commands). " - "Set via LIBTMUX_SAFETY env var." + "Set via LIBTMUX_SAFETY env var. " + "Tools outside the active tier are hidden and will not appear in " + "tool listings." ) # Agent tmux context From 0e681dbcbab2536994752329cc21f8e842670955 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 16:33:28 -0500 Subject: [PATCH 04/19] docs(quickstart): Add "How it works" section with tool sequence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Quickstart showed natural-language prompts but never explained the tool sequence agents execute. The send → wait → capture pattern is the fundamental workflow and should be taught on the first page. what: - Add "How it works" section between "Try it" and "Next steps" - Shows the 3-step pattern: send_keys → wait_for_text → capture_pane - Links to individual tool docs via {ref} --- docs/quickstart.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/quickstart.md b/docs/quickstart.md index 44a162b..5a76ace 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -30,6 +30,16 @@ Here are a few things to try: > Search all my panes for the word "error". +## How it works + +When you say "run `make test` and show me the output", the agent executes a three-step pattern: + +1. {ref}`send-keys` — send the command to a tmux pane +2. {ref}`wait-for-text` — wait for the shell prompt to return (command finished) +3. {ref}`capture-pane` — read the terminal output + +This **send → wait → capture** sequence is the fundamental workflow. Most agent interactions with tmux follow this pattern or a variation of it. + ## Next steps - {ref}`concepts` — Understand the tmux hierarchy and how tools target panes From 7d7aa4b6d308601d3eeac2f1fa4c0cb0d6f68bf4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 16:34:09 -0500 Subject: [PATCH 05/19] docs(tools): Add "Which tool do I want?" decision guide why: The tools index showed a flat catalog grouped by safety tier, but didn't help users select the right tool for their intent. what: - Add intent-based decision guide above the Inspect/Act/Destroy grid - Groups by task: reading content, running commands, creating structure - Uses {tool} role for badged links, {envvar} for env var ref --- docs/tools/index.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/tools/index.md b/docs/tools/index.md index 41b057b..334b593 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -2,7 +2,27 @@ # Tools -All tools accept an optional `socket_name` parameter for multi-server support. It defaults to the `LIBTMUX_SOCKET` env var. See {ref}`configuration`. +All tools accept an optional `socket_name` parameter for multi-server support. It defaults to the {envvar}`LIBTMUX_SOCKET` env var. See {ref}`configuration`. + +## Which tool do I want? + +**Reading terminal content?** +- Know which pane? → {tool}`capture-pane` +- Don't know which pane? → {tool}`search-panes` +- Need to wait for output? → {tool}`wait-for-text` +- Only need metadata (PID, path, size)? → {tool}`get-pane-info` + +**Running a command?** +- {tool}`send-keys` — then {tool}`wait-for-text` + {tool}`capture-pane` + +**Creating workspace structure?** +- New session → {tool}`create-session` +- New window → {tool}`create-window` +- New pane → {tool}`split-window` + +**Changing settings?** +- tmux options → {tool}`show-option` / {tool}`set-option` +- Environment vars → {ref}`show-environment` / {ref}`set-environment` ## Inspect From a8544734fa6ee3d8cbe4e41a61a4a20e84a760c6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 16:36:41 -0500 Subject: [PATCH 06/19] docs(tools): Add response examples to 15 tools that lacked them why: 15 of 27 tools had no response examples, making it hard for agents and humans to predict output shapes without trial calls. what: - panes.md: get_pane_info, set_pane_title, clear_pane, resize_pane, kill_pane - sessions.md: get_server_info, kill_session, kill_server - windows.md: list_panes, rename_window, select_layout, resize_window, kill_window - options.md: show_environment, set_environment - All responses derived from real MCP outputs, anonymized --- docs/tools/options.md | 45 ++++++++++++ docs/tools/panes.md | 126 ++++++++++++++++++++++++++++++++++ docs/tools/sessions.md | 53 ++++++++++++++ docs/tools/windows.md | 152 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 376 insertions(+) diff --git a/docs/tools/options.md b/docs/tools/options.md index 2242c5b..5c3c1b8 100644 --- a/docs/tools/options.md +++ b/docs/tools/options.md @@ -42,6 +42,29 @@ Response: **Side effects:** None. Readonly. +**Example:** + +```json +{ + "tool": "show_environment", + "arguments": {} +} +``` + +Response: + +```json +{ + "variables": { + "SHELL": "/bin/zsh", + "TERM": "xterm-256color", + "HOME": "/home/user", + "USER": "user", + "LANG": "C.UTF-8" + } +} +``` + ```{fastmcp-tool-input} env_tools.show_environment ``` @@ -89,5 +112,27 @@ Response: **Side effects:** Sets the variable in the tmux server. +**Example:** + +```json +{ + "tool": "set_environment", + "arguments": { + "name": "MY_VAR", + "value": "hello" + } +} +``` + +Response: + +```json +{ + "name": "MY_VAR", + "value": "hello", + "status": "set" +} +``` + ```{fastmcp-tool-input} env_tools.set_environment ``` diff --git a/docs/tools/panes.md b/docs/tools/panes.md index d3b34e9..b927b1f 100644 --- a/docs/tools/panes.md +++ b/docs/tools/panes.md @@ -55,6 +55,36 @@ other metadata without reading the terminal content. **Side effects:** None. Readonly. +**Example:** + +```json +{ + "tool": "get_pane_info", + "arguments": { + "pane_id": "%0" + } +} +``` + +Response: + +```json +{ + "pane_id": "%0", + "pane_index": "0", + "pane_width": "80", + "pane_height": "24", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + ```{fastmcp-tool-input} pane_tools.get_pane_info ``` @@ -195,6 +225,37 @@ Keys sent to pane %2 **Side effects:** Changes the pane title. +**Example:** + +```json +{ + "tool": "set_pane_title", + "arguments": { + "pane_id": "%0", + "title": "build" + } +} +``` + +Response: + +```json +{ + "pane_id": "%0", + "pane_index": "0", + "pane_width": "80", + "pane_height": "24", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "build", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + ```{fastmcp-tool-input} pane_tools.set_pane_title ``` @@ -207,6 +268,23 @@ Keys sent to pane %2 **Side effects:** Clears the pane's visible content. +**Example:** + +```json +{ + "tool": "clear_pane", + "arguments": { + "pane_id": "%0" + } +} +``` + +Response (string): + +```text +Pane cleared: %0 +``` + ```{fastmcp-tool-input} pane_tools.clear_pane ``` @@ -219,6 +297,37 @@ Keys sent to pane %2 **Side effects:** Changes pane size. May affect adjacent panes. +**Example:** + +```json +{ + "tool": "resize_pane", + "arguments": { + "pane_id": "%0", + "height": 15 + } +} +``` + +Response: + +```json +{ + "pane_id": "%0", + "pane_index": "0", + "pane_width": "80", + "pane_height": "15", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + ```{fastmcp-tool-input} pane_tools.resize_pane ``` @@ -234,5 +343,22 @@ without affecting sibling panes. **Side effects:** Destroys the pane. Not reversible. +**Example:** + +```json +{ + "tool": "kill_pane", + "arguments": { + "pane_id": "%1" + } +} +``` + +Response (string): + +```text +Pane killed: %1 +``` + ```{fastmcp-tool-input} pane_tools.kill_pane ``` diff --git a/docs/tools/sessions.md b/docs/tools/sessions.md index 539c3cb..c01a031 100644 --- a/docs/tools/sessions.md +++ b/docs/tools/sessions.md @@ -51,6 +51,27 @@ or inspect server-level state before creating sessions. **Side effects:** None. Readonly. +**Example:** + +```json +{ + "tool": "get_server_info", + "arguments": {} +} +``` + +Response: + +```json +{ + "is_alive": true, + "socket_name": null, + "socket_path": null, + "session_count": 2, + "version": "3.6a" +} +``` + ```{fastmcp-tool-input} server_tools.get_server_info ``` @@ -141,6 +162,23 @@ windows and panes in the session. **Side effects:** Destroys the session and all its contents. Not reversible. +**Example:** + +```json +{ + "tool": "kill_session", + "arguments": { + "session_name": "old-workspace" + } +} +``` + +Response (string): + +```text +Session killed: old-workspace +``` + ```{fastmcp-tool-input} session_tools.kill_session ``` @@ -156,5 +194,20 @@ session, window, and pane. **Side effects:** Destroys everything. Not reversible. +**Example:** + +```json +{ + "tool": "kill_server", + "arguments": {} +} +``` + +Response (string): + +```text +Server killed +``` + ```{fastmcp-tool-input} server_tools.kill_server ``` diff --git a/docs/tools/windows.md b/docs/tools/windows.md index 9e9c71e..0723d7b 100644 --- a/docs/tools/windows.md +++ b/docs/tools/windows.md @@ -67,6 +67,52 @@ sending keys or capturing output. **Side effects:** None. Readonly. +**Example:** + +```json +{ + "tool": "list_panes", + "arguments": { + "session_name": "dev" + } +} +``` + +Response: + +```json +[ + { + "pane_id": "%0", + "pane_index": "0", + "pane_width": "80", + "pane_height": "15", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "build", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null + }, + { + "pane_id": "%1", + "pane_index": "1", + "pane_width": "80", + "pane_height": "8", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12400", + "pane_title": "", + "pane_active": "0", + "window_id": "@0", + "session_id": "$0", + "is_caller": null + } +] +``` + ```{fastmcp-tool-input} window_tools.list_panes ``` @@ -164,6 +210,35 @@ Response: **Side effects:** Renames the window. +**Example:** + +```json +{ + "tool": "rename_window", + "arguments": { + "session_name": "dev", + "new_name": "build" + } +} +``` + +Response: + +```json +{ + "window_id": "@0", + "window_name": "build", + "window_index": "1", + "session_id": "$0", + "session_name": "dev", + "pane_count": 2, + "window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]", + "window_active": "1", + "window_width": "80", + "window_height": "24" +} +``` + ```{fastmcp-tool-input} window_tools.rename_window ``` @@ -177,6 +252,35 @@ Response: **Side effects:** Rearranges all panes in the window. +**Example:** + +```json +{ + "tool": "select_layout", + "arguments": { + "session_name": "dev", + "layout": "even-vertical" + } +} +``` + +Response: + +```json +{ + "window_id": "@0", + "window_name": "editor", + "window_index": "1", + "session_id": "$0", + "session_name": "dev", + "pane_count": 2, + "window_layout": "even-vertical,80x24,0,0[80x12,0,0,0,80x11,0,13,1]", + "window_active": "1", + "window_width": "80", + "window_height": "24" +} +``` + ```{fastmcp-tool-input} window_tools.select_layout ``` @@ -189,6 +293,36 @@ Response: **Side effects:** Changes window size. +**Example:** + +```json +{ + "tool": "resize_window", + "arguments": { + "session_name": "dev", + "width": 120, + "height": 40 + } +} +``` + +Response: + +```json +{ + "window_id": "@0", + "window_name": "editor", + "window_index": "1", + "session_id": "$0", + "session_name": "dev", + "pane_count": 2, + "window_layout": "baaa,120x40,0,0[120x20,0,0,0,120x19,0,21,1]", + "window_active": "1", + "window_width": "120", + "window_height": "40" +} +``` + ```{fastmcp-tool-input} window_tools.resize_window ``` @@ -203,5 +337,23 @@ Response: **Side effects:** Destroys the window and all its panes. Not reversible. +**Example:** + +```json +{ + "tool": "kill_window", + "arguments": { + "session_name": "dev", + "window_name": "old-logs" + } +} +``` + +Response (string): + +```text +Window killed: old-logs +``` + ```{fastmcp-tool-input} window_tools.kill_window ``` From 1c69e6ba1b6a463d04e146d6fdf0ca7a1d967919 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 16:37:46 -0500 Subject: [PATCH 07/19] docs: Add recipes page with 6 composable workflow patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Tool docs explained atoms but never showed molecules. The send_keys → wait_for_text → capture_pane pattern — the fundamental workflow — appeared nowhere as a complete sequence. what: - Run a command and capture output (the core pattern + race condition warning) - Start a service and wait for readiness (Playwright/web server use case) - Search for errors across all panes (two-phase: search broad, capture narrow) - Set up a multi-pane workspace (create, split, layout, label) - Interrupt a hung command (Ctrl-C with enter=false) - Clean capture without prior output (clear, send, wait, capture) - Added to "Use it" toctree between tools/index and configuration --- docs/index.md | 1 + docs/recipes.md | 156 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 docs/recipes.md diff --git a/docs/index.md b/docs/index.md index 497b1b3..1bc8ff4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -93,6 +93,7 @@ clients :caption: Use it tools/index +recipes configuration ``` diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 0000000..8a02d18 --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,156 @@ +(recipes)= + +# Recipes + +Composable workflow patterns showing how to combine tools. Each recipe shows the tool sequence, why each step matters, and a common mistake to avoid. + +## Run a command and capture output + +The fundamental workflow. Most agent interactions follow this pattern. + +```json +{"tool": "send_keys", "arguments": {"keys": "make test", "pane_id": "%0"}} +``` + +```json +{"tool": "wait_for_text", "arguments": {"pattern": "\\$\\s*$", "pane_id": "%0", "regex": true}} +``` + +```json +{"tool": "capture_pane", "arguments": {"pane_id": "%0", "start": -50}} +``` + +**Why each step matters:** +- `send_keys` sends the command but returns immediately — it does not wait for completion +- `wait_for_text` blocks until the shell prompt returns (the `$` regex), confirming the command finished +- `capture_pane` reads the result after the command has completed + +```{warning} +Do not call `capture_pane` immediately after `send_keys`. There is a race condition — you may capture the terminal *before* the command produces output. Always use `wait_for_text` between them. +``` + +## Start a service and wait for readiness + +Use when you need a background service running before proceeding — web servers, databases, build watchers. + +```json +{"tool": "split_window", "arguments": {"session_name": "dev", "direction": "right"}} +``` + +The new pane's `pane_id` is in the response. Use it for the remaining steps: + +```json +{"tool": "send_keys", "arguments": {"keys": "npm run dev", "pane_id": "%1"}} +``` + +```json +{"tool": "wait_for_text", "arguments": {"pattern": "Local:.*http://localhost", "pane_id": "%1", "regex": true, "timeout": 30}} +``` + +Now the server is ready — run tests in the original pane: + +```json +{"tool": "send_keys", "arguments": {"keys": "npx playwright test", "pane_id": "%0"}} +``` + +**Common mistake:** Using a fixed `sleep` instead of `wait_for_text`. Server startup times vary — `wait_for_text` adapts automatically. + +## Search for errors across all panes + +Find which pane has an error without knowing where to look. + +```json +{"tool": "search_panes", "arguments": {"pattern": "error", "session_name": "dev"}} +``` + +The response lists every pane with matching lines. Then capture the full context from each match: + +```json +{"tool": "capture_pane", "arguments": {"pane_id": "%2", "start": -100}} +``` + +**Why two steps:** `search_panes` is fast — it uses tmux's built-in filter for plain text patterns and never captures full pane content. Once you know *which* pane has the error, `capture_pane` gets the full context. + +**Common mistake:** Using `list_panes` to find errors. `list_panes` only searches metadata (names, IDs, current command) — not terminal content. + +## Set up a multi-pane workspace + +Create a structured development layout with labeled panes. + +```json +{"tool": "create_session", "arguments": {"session_name": "workspace"}} +``` + +```json +{"tool": "split_window", "arguments": {"session_name": "workspace", "direction": "right"}} +``` + +```json +{"tool": "split_window", "arguments": {"session_name": "workspace", "direction": "below"}} +``` + +```json +{"tool": "select_layout", "arguments": {"session_name": "workspace", "layout": "main-vertical"}} +``` + +Label each pane for later identification: + +```json +{"tool": "set_pane_title", "arguments": {"pane_id": "%0", "title": "editor"}} +``` + +```json +{"tool": "set_pane_title", "arguments": {"pane_id": "%1", "title": "server"}} +``` + +```json +{"tool": "set_pane_title", "arguments": {"pane_id": "%2", "title": "tests"}} +``` + +Then start processes in each: + +```json +{"tool": "send_keys", "arguments": {"keys": "vim .", "pane_id": "%0"}} +``` + +```json +{"tool": "send_keys", "arguments": {"keys": "npm run dev", "pane_id": "%1"}} +``` + +## Interrupt a hung command + +Recover when a command is stuck or waiting for input. + +```json +{"tool": "send_keys", "arguments": {"keys": "C-c", "pane_id": "%0", "enter": false}} +``` + +```json +{"tool": "wait_for_text", "arguments": {"pattern": "\\$\\s*$", "pane_id": "%0", "regex": true, "timeout": 5}} +``` + +**Why `enter: false`:** Ctrl-C is a tmux key name, not text to type. Setting `enter: false` prevents sending an extra Enter keystroke after the interrupt signal. + +**If the interrupt fails** (process ignores Ctrl-C), use `kill_pane` to destroy the pane and `split_window` to get a fresh one. + +## Clean capture (no prior output) + +Get a clean capture without output from previous commands. + +```json +{"tool": "clear_pane", "arguments": {"pane_id": "%0"}} +``` + +```json +{"tool": "send_keys", "arguments": {"keys": "pytest -x", "pane_id": "%0"}} +``` + +```json +{"tool": "wait_for_text", "arguments": {"pattern": "passed|failed|error", "pane_id": "%0", "regex": true, "timeout": 60}} +``` + +```json +{"tool": "capture_pane", "arguments": {"pane_id": "%0"}} +``` + +**Why clear first:** Without clearing, `capture_pane` returns the visible viewport which may include output from prior commands. Clearing ensures you only capture output from the command you just ran. From 0b487ffc566c5dde0a4240af02cfde5fabc90c78 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 16:38:55 -0500 Subject: [PATCH 08/19] =?UTF-8?q?docs(topics):=20Add=20gotchas=20page=20?= =?UTF-8?q?=E2=80=94=207=20things=20that=20will=20bite=20you?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Troubleshooting is symptom-based (after something breaks). Gotchas are preventive — things you should know before they bite. what: - Metadata vs. content (list_panes vs search_panes) - send_keys sends Enter by default (enter=false for C-c) - capture_pane race condition after send_keys - Window names not unique across sessions (use IDs) - Pane IDs globally unique but ephemeral (reset on server restart) - suppress_history requires HISTCONTROL=ignorespace - clear_pane is two tmux commands with a brief gap - Added to topics/ toctree and grid cards --- docs/topics/gotchas.md | 64 ++++++++++++++++++++++++++++++++++++++++++ docs/topics/index.md | 7 +++++ 2 files changed, 71 insertions(+) create mode 100644 docs/topics/gotchas.md diff --git a/docs/topics/gotchas.md b/docs/topics/gotchas.md new file mode 100644 index 0000000..f369f74 --- /dev/null +++ b/docs/topics/gotchas.md @@ -0,0 +1,64 @@ +(gotchas)= + +# Gotchas + +Things that will bite you if you don't know about them in advance. For symptom-based debugging, see {ref}`troubleshooting `. + +## Metadata vs. content + +{ref}`list-panes` and {ref}`list-windows` search **metadata** — names, IDs, current command. They do not search what is displayed in the terminal. + +To find text that is visible in terminals, use {tool}`search-panes`. To read what a specific pane shows, use {tool}`capture-pane`. + +This is the most common source of agent confusion. The server instructions already warn about this, but it bears repeating: if a user asks "which pane mentions error", the answer is `search_panes`, not `list_panes`. + +## `send_keys` sends Enter by default + +When you call `send_keys` with `keys: "C-c"`, it sends Ctrl-C **and then presses Enter**. For control sequences, set `enter: false`: + +```json +{"tool": "send_keys", "arguments": {"keys": "C-c", "pane_id": "%0", "enter": false}} +``` + +The `enter` parameter defaults to `true`, which is correct for commands (`make test` + Enter) but wrong for control keys, partial input, or key sequences. + +## `capture_pane` after `send_keys` is a race condition + +`send_keys` returns immediately after sending keystrokes to tmux. It does **not** wait for the command to execute or produce output. + +```json +{"tool": "send_keys", "arguments": {"keys": "pytest", "pane_id": "%0"}} +{"tool": "capture_pane", "arguments": {"pane_id": "%0"}} +``` + +The capture above may return the terminal state **before** pytest runs. Use {tool}`wait-for-text` between them: + +```json +{"tool": "send_keys", "arguments": {"keys": "pytest", "pane_id": "%0"}} +{"tool": "wait_for_text", "arguments": {"pattern": "passed|failed|error", "pane_id": "%0", "regex": true}} +{"tool": "capture_pane", "arguments": {"pane_id": "%0"}} +``` + +See {ref}`recipes` for the complete pattern. + +## Window names are not unique across sessions + +Two sessions can each have a window named "editor". Targeting by `window_name` alone is ambiguous — always include `session_name` or use the globally unique `window_id` (e.g., `@0`, `@1`). + +Pane IDs (`%0`, `%1`, etc.) are globally unique and are the preferred targeting method. + +## Pane IDs are globally unique but ephemeral + +Pane IDs like `%0`, `%5`, `%12` are unique across all sessions and windows within a tmux server. They do not reset when windows are created or destroyed. + +However, they reset when the tmux **server** restarts. Do not cache pane IDs across server restarts. After killing and recreating a session, re-discover pane IDs with {ref}`list-panes`. + +## `suppress_history` requires shell support + +The `suppress_history` parameter on `send_keys` prepends a space before the command, which prevents it from being saved in shell history. This only works if the shell's `HISTCONTROL` variable includes `ignorespace` (the default for bash, but not universal across all shells). + +## `clear_pane` is not fully atomic + +`clear_pane` runs two tmux commands in sequence: `send-keys -R` (reset terminal) then `clear-history` (clear scrollback). There is a brief gap between them where partial content may be visible. + +For most use cases this is not a problem. If you need guaranteed clean state, add a small delay before the next `capture_pane`. diff --git a/docs/topics/index.md b/docs/topics/index.md index bc7cb9e..52d76b3 100644 --- a/docs/topics/index.md +++ b/docs/topics/index.md @@ -29,6 +29,12 @@ Three-tier safety system for controlling tool access. Symptom-based guide for common issues. ::: +:::{grid-item-card} Gotchas +:link: gotchas +:link-type: doc +Things that will bite you if you don't know about them. +::: + :::: ```{toctree} @@ -37,5 +43,6 @@ Symptom-based guide for common issues. architecture concepts safety +gotchas troubleshooting ``` From a31a376fdf6c91146b4745138cfc2977bea28b64 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 16:40:25 -0500 Subject: [PATCH 09/19] docs(topics): Add agent prompting guide why: No MCP server docs help humans write effective agent instructions. The server's _BASE_INSTRUCTIONS are invisible in the docs, and there is no guidance on prompt patterns, anti-patterns, or system prompt fragments. what: - Print _BASE_INSTRUCTIONS verbatim with explanation of each part - Document dynamic context: safety tier, caller pane awareness - 6 effective prompt patterns with expected tool sequences - 4 anti-patterns with better alternatives - 3 copy-pasteable system prompt fragments (general, safe, dev) - 5 tool selection heuristics for agent decision-making - Added to topics/ toctree and grid cards --- docs/topics/gotchas.md | 2 +- docs/topics/index.md | 7 +++ docs/topics/prompting.md | 92 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 docs/topics/prompting.md diff --git a/docs/topics/gotchas.md b/docs/topics/gotchas.md index f369f74..b33c42b 100644 --- a/docs/topics/gotchas.md +++ b/docs/topics/gotchas.md @@ -6,7 +6,7 @@ Things that will bite you if you don't know about them in advance. For symptom-b ## Metadata vs. content -{ref}`list-panes` and {ref}`list-windows` search **metadata** — names, IDs, current command. They do not search what is displayed in the terminal. +{tool}`list-panes` and {tool}`list-windows` search **metadata** — names, IDs, current command. They do not search what is displayed in the terminal. To find text that is visible in terminals, use {tool}`search-panes`. To read what a specific pane shows, use {tool}`capture-pane`. diff --git a/docs/topics/index.md b/docs/topics/index.md index 52d76b3..ebd3569 100644 --- a/docs/topics/index.md +++ b/docs/topics/index.md @@ -35,6 +35,12 @@ Symptom-based guide for common issues. Things that will bite you if you don't know about them. ::: +:::{grid-item-card} Agent Prompting +:link: prompting +:link-type: doc +Write effective instructions for AI agents using tmux tools. +::: + :::: ```{toctree} @@ -44,5 +50,6 @@ architecture concepts safety gotchas +prompting troubleshooting ``` diff --git a/docs/topics/prompting.md b/docs/topics/prompting.md new file mode 100644 index 0000000..cb8fc1f --- /dev/null +++ b/docs/topics/prompting.md @@ -0,0 +1,92 @@ +(prompting)= + +# Agent prompting guide + +How to write effective instructions for AI agents using libtmux-mcp. + +## What the server tells your agent automatically + +Every MCP client receives these instructions when connecting to the libtmux-mcp server. You do not need to repeat this information — the agent already knows it. + +```text +libtmux MCP server for programmatic tmux control. tmux hierarchy: +Server > Session > Window > Pane. Use pane_id (e.g. '%1') as the +preferred targeting method - it is globally unique within a tmux server. +Use send_keys to execute commands and capture_pane to read output. All +tools accept an optional socket_name parameter for multi-server support +(defaults to LIBTMUX_SOCKET env var). + +IMPORTANT — metadata vs content: list_windows, list_panes, and +list_sessions only search metadata (names, IDs, current command). To +find text that is actually visible in terminals — when users ask what +panes 'contain', 'mention', 'show', or 'have' — use search_panes to +search across all pane contents, or list_panes + capture_pane on each +pane for manual inspection. +``` + +The server also dynamically adds: +- **Safety tier context**: Which tier is active and what tools are available +- **Caller pane awareness**: If the server runs inside tmux, it tells the agent which pane is its own (via `TMUX_PANE`) + +## Effective prompt patterns + +These natural-language prompts reliably trigger the right tool sequences: + +| Prompt | Agent interprets as | +|--------|-------------------| +| "Run `pytest` in my build pane and show results" | {tool}`send-keys` → {tool}`wait-for-text` → {tool}`capture-pane` | +| "Start the dev server and wait until it's ready" | {tool}`send-keys` → {tool}`wait-for-text` (for "listening on") | +| "Check if any pane has errors" | {tool}`search-panes` with pattern "error" | +| "Set up a workspace with editor, server, and tests" | {tool}`create-session` → {tool}`split-window` (x2) → {tool}`set-pane-title` (x3) | +| "What's running in my tmux sessions?" | {tool}`list-sessions` → {tool}`list-panes` → {tool}`capture-pane` | +| "Kill the old workspace session" | {tool}`kill-session` (after confirming target) | + +## Anti-patterns to avoid + +| Prompt | Problem | Better version | +|--------|---------|---------------| +| "Run this command" | Ambiguous — agent may use its own shell instead of tmux | "Run `make test` in a tmux pane" | +| "Check my terminal" | Which pane? Agent must discover first | "Check the pane running `npm dev`" or "Search all panes for errors" | +| "Clean up everything" | Too broad for destructive operations | "Kill the `ci-test` session" | +| "Show me the output" | Capture immediately? Or wait? | "Wait for the command to finish, then show me the output" | + +## System prompt fragments + +Copy these into your agent's system instructions (`CLAUDE.md`, `.cursorrules`, or MCP client config) to improve behavior: + +### For general tmux workflows + +```text +When executing long-running commands (servers, builds, test suites), +use tmux via the libtmux MCP server rather than running them directly. +This keeps output accessible for later inspection. Use the pattern: +send_keys → wait_for_text (for completion signal) → capture_pane. +``` + +### For safe agent behavior + +```text +Before creating tmux sessions, check list_sessions to avoid duplicates. +Always use pane_id for targeting — it is globally unique. Never run +destructive operations (kill_session, kill_server) without confirming +the target with the user first. +``` + +### For development workflows + +```text +When the user asks you to run tests or start servers, use dedicated +tmux panes. Split windows to run related processes side-by-side. +Use wait_for_text to know when a server is ready before running tests +that depend on it. +``` + +## Tool selection heuristics + +When an agent is unsure which tool to use, these rules help: + +1. **Discovery first**: Call {tool}`list-sessions` or {tool}`list-panes` before acting on specific targets +2. **Prefer IDs**: Once you have a `pane_id`, use it for all subsequent calls — it never changes during the pane's lifetime +3. **Wait, don't poll**: Use {tool}`wait-for-text` instead of repeatedly calling {tool}`capture-pane` in a loop +4. **Content vs. metadata**: If looking for text *in* a terminal, use {tool}`search-panes`. If looking for pane *properties* (name, PID, path), use {tool}`list-panes` or {tool}`get-pane-info` +5. **Destructive tools are opt-in**: Never kill sessions, windows, or panes unless the user explicitly asks From 87efa1d358e18f32c90bbddd473819e39402f6cc Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 17:09:24 -0500 Subject: [PATCH 10/19] =?UTF-8?q?feat(docs[=5Fext]):=20Add=20{toolref}=20r?= =?UTF-8?q?ole=20=E2=80=94=20code-linked=20tool=20refs=20without=20badge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: {ref} renders as plain text (Sphinx hardcodes nodes.inline in build_reference_node). {tool} renders as code+badge. Needed a middle ground: code-formatted linked tool names without badges for dense contexts like tables and inline prose. what: - Add {toolref} role: resolves same labels as {tool}, renders as inside , no safety badge - Reuse _tool_ref_placeholder with show_badge=False - Update prompting.md tables and heuristics to use {toolref} --- docs/_ext/fastmcp_autodoc.py | 43 ++++++++++++++++++++++++++++++------ docs/topics/prompting.md | 18 +++++++-------- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/docs/_ext/fastmcp_autodoc.py b/docs/_ext/fastmcp_autodoc.py index aca3053..948fbac 100644 --- a/docs/_ext/fastmcp_autodoc.py +++ b/docs/_ext/fastmcp_autodoc.py @@ -869,7 +869,11 @@ def _add_section_badges( class _tool_ref_placeholder(nodes.General, nodes.Inline, nodes.Element): - """Placeholder node for ``{tool}`` role, resolved at doctree-resolved.""" + """Placeholder node for ``{tool}`` and ``{toolref}`` roles. + + Resolved at ``doctree-resolved`` by ``_resolve_tool_refs``. + The ``show_badge`` attribute controls whether the safety badge is appended. + """ def _resolve_tool_refs( @@ -877,7 +881,10 @@ def _resolve_tool_refs( doctree: nodes.document, fromdocname: str, ) -> None: - """Resolve ``{tool}`` placeholders into links with safety badges. + """Resolve ``{tool}`` and ``{toolref}`` placeholders into links. + + ``{tool}`` renders as ``code`` + safety badge. + ``{toolref}`` renders as ``code`` only (no badge). Runs at ``doctree-resolved`` — after all labels are registered and standard ``{ref}`` resolution is done. @@ -888,6 +895,7 @@ def _resolve_tool_refs( for node in list(doctree.findall(_tool_ref_placeholder)): target = node.get("reftarget", "") + show_badge = node.get("show_badge", True) label_info = domain.labels.get(target) if label_info is None: node.replace_self(nodes.literal("", target.replace("-", "_"))) @@ -908,10 +916,11 @@ def _resolve_tool_refs( newnode += nodes.literal("", tool_name) - tool_info = tool_data.get(tool_name) - if tool_info: - newnode += nodes.Text(" ") - newnode += _safety_badge(tool_info.safety) + if show_badge: + tool_info = tool_data.get(tool_name) + if tool_info: + newnode += nodes.Text(" ") + newnode += _safety_badge(tool_info.safety) node.replace_self(newnode) @@ -930,7 +939,26 @@ def _tool_role( Creates a placeholder node resolved later by ``_resolve_tool_refs``. """ target = text.strip().replace("_", "-") - node = _tool_ref_placeholder(rawtext, reftarget=target) + node = _tool_ref_placeholder(rawtext, reftarget=target, show_badge=True) + return [node], [] + + +def _toolref_role( + name: str, + rawtext: str, + text: str, + lineno: int, + inliner: object, + options: dict[str, object] | None = None, + content: list[str] | None = None, +) -> tuple[list[nodes.Node], list[nodes.system_message]]: + """Inline role ``:toolref:`capture-pane``` → code-linked tool name, no badge. + + Like ``{tool}`` but without the safety badge. Use in dense contexts + (tables, inline prose) where badges would be too heavy. + """ + target = text.strip().replace("_", "-") + node = _tool_ref_placeholder(rawtext, reftarget=target, show_badge=False) return [node], [] @@ -958,6 +986,7 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.connect("doctree-resolved", _add_section_badges) app.connect("doctree-resolved", _resolve_tool_refs) app.add_role("tool", _tool_role) + app.add_role("toolref", _toolref_role) app.add_role("badge", _badge_role) app.add_directive("fastmcp-tool", FastMCPToolDirective) app.add_directive("fastmcp-tool-input", FastMCPToolInputDirective) diff --git a/docs/topics/prompting.md b/docs/topics/prompting.md index cb8fc1f..a3180de 100644 --- a/docs/topics/prompting.md +++ b/docs/topics/prompting.md @@ -34,12 +34,12 @@ These natural-language prompts reliably trigger the right tool sequences: | Prompt | Agent interprets as | |--------|-------------------| -| "Run `pytest` in my build pane and show results" | {tool}`send-keys` → {tool}`wait-for-text` → {tool}`capture-pane` | -| "Start the dev server and wait until it's ready" | {tool}`send-keys` → {tool}`wait-for-text` (for "listening on") | -| "Check if any pane has errors" | {tool}`search-panes` with pattern "error" | -| "Set up a workspace with editor, server, and tests" | {tool}`create-session` → {tool}`split-window` (x2) → {tool}`set-pane-title` (x3) | -| "What's running in my tmux sessions?" | {tool}`list-sessions` → {tool}`list-panes` → {tool}`capture-pane` | -| "Kill the old workspace session" | {tool}`kill-session` (after confirming target) | +| "Run `pytest` in my build pane and show results" | {toolref}`send-keys` → {toolref}`wait-for-text` → {toolref}`capture-pane` | +| "Start the dev server and wait until it's ready" | {toolref}`send-keys` → {toolref}`wait-for-text` (for "listening on") | +| "Check if any pane has errors" | {toolref}`search-panes` with pattern "error" | +| "Set up a workspace with editor, server, and tests" | {toolref}`create-session` → {toolref}`split-window` (x2) → {toolref}`set-pane-title` (x3) | +| "What's running in my tmux sessions?" | {toolref}`list-sessions` → {toolref}`list-panes` → {toolref}`capture-pane` | +| "Kill the old workspace session" | {toolref}`kill-session` (after confirming target) | ## Anti-patterns to avoid @@ -85,8 +85,8 @@ that depend on it. When an agent is unsure which tool to use, these rules help: -1. **Discovery first**: Call {tool}`list-sessions` or {tool}`list-panes` before acting on specific targets +1. **Discovery first**: Call {toolref}`list-sessions` or {toolref}`list-panes` before acting on specific targets 2. **Prefer IDs**: Once you have a `pane_id`, use it for all subsequent calls — it never changes during the pane's lifetime -3. **Wait, don't poll**: Use {tool}`wait-for-text` instead of repeatedly calling {tool}`capture-pane` in a loop -4. **Content vs. metadata**: If looking for text *in* a terminal, use {tool}`search-panes`. If looking for pane *properties* (name, PID, path), use {tool}`list-panes` or {tool}`get-pane-info` +3. **Wait, don't poll**: Use {toolref}`wait-for-text` instead of repeatedly calling {toolref}`capture-pane` in a loop +4. **Content vs. metadata**: If looking for text *in* a terminal, use {toolref}`search-panes`. If looking for pane *properties* (name, PID, path), use {toolref}`list-panes` or {toolref}`get-pane-info` 5. **Destructive tools are opt-in**: Never kill sessions, windows, or panes unless the user explicitly asks From 9054795edc08205ec44e097e7a51d65a01083940 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 17:16:28 -0500 Subject: [PATCH 11/19] fix(docs[_ext]): Suppress mypy misc errors on docutils node subclasses why: CI has docutils type stubs that expose General/Inline/Element as Any, causing "Class cannot subclass" mypy errors on node subclasses. what: - Add type: ignore[misc] to _safety_badge_node and _tool_ref_placeholder --- docs/_ext/fastmcp_autodoc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_ext/fastmcp_autodoc.py b/docs/_ext/fastmcp_autodoc.py index 948fbac..f6d0811 100644 --- a/docs/_ext/fastmcp_autodoc.py +++ b/docs/_ext/fastmcp_autodoc.py @@ -421,7 +421,7 @@ def _extract_enum_values(type_str: str) -> list[str]: return values -class _safety_badge_node(nodes.General, nodes.Inline, nodes.Element): +class _safety_badge_node(nodes.General, nodes.Inline, nodes.Element): # type: ignore[misc] """Custom node for safety badges with ARIA attributes in HTML output.""" @@ -868,7 +868,7 @@ def _add_section_badges( title_node += _safety_badge(tier) -class _tool_ref_placeholder(nodes.General, nodes.Inline, nodes.Element): +class _tool_ref_placeholder(nodes.General, nodes.Inline, nodes.Element): # type: ignore[misc] """Placeholder node for ``{tool}`` and ``{toolref}`` roles. Resolved at ``doctree-resolved`` by ``_resolve_tool_refs``. From 7fe9712dd1b22e49a94272e6cbb4f8003ce15cf1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 18:49:25 -0500 Subject: [PATCH 12/19] docs(recipes): Rewrite as scenario-driven workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Recipes showed raw JSON tool calls that didn't reflect how developers actually use the tools. Real usage is scenario-driven: discover existing state, decide what to do, then act. Refined through brainstorm-and-refine session (9 originals from Claude/Gemini/GPT, 3 refinement passes, converged at pass 3). what: - Replace 10 JSON-heavy recipes with 6 scenario-driven workflows - Lead with real ~/work/cv Vite/Playwright discovery example - Each recipe: Situation → Discover → Decide → Act → The non-obvious part - Tool calls embedded in reasoning narrative, not standalone JSON - Uses {tool} for badged refs, {toolref} for inline prose refs - All params verified against source across 3 refinement passes - Closes with "What to read next" linking to prompting and gotchas --- docs/recipes.md | 364 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 267 insertions(+), 97 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 8a02d18..249f808 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -2,155 +2,325 @@ # Recipes -Composable workflow patterns showing how to combine tools. Each recipe shows the tool sequence, why each step matters, and a common mistake to avoid. +Each recipe starts from a real workspace situation and traces the agent's +reasoning through discovery, decision, and action. The goal is not to show +tool-call sequences -- it is to show *how an agent decides* what to do with +existing tmux state so you can write better prompts and system instructions. -## Run a command and capture output +Every recipe uses the same structure: -The fundamental workflow. Most agent interactions follow this pattern. +- **Situation** -- the developer's world before the agent acts +- **Discover** -- what the agent inspects and why +- **Decide** -- the judgment call that changes the plan +- **Act** -- the minimum safe action sequence +- **The non-obvious part** -- the lesson you would miss from reading tool docs + alone +- **Prompt** -- a natural-language sentence that triggers this recipe -```json -{"tool": "send_keys", "arguments": {"keys": "make test", "pane_id": "%0"}} -``` +--- -```json -{"tool": "wait_for_text", "arguments": {"pattern": "\\$\\s*$", "pane_id": "%0", "regex": true}} -``` +## 1. Find a running dev server and test against it + +**Situation.** A developer manages a CV project with tmuxp. One pane is +already running `pnpm start` with Vite somewhere in the `react` window. +They want to run Playwright e2e tests. The agent does not know which pane +has the server, or what port it chose. + +### Discover + +> {toolref}`list-panes` will not help here -- it shows metadata like current +> command and working directory, not terminal content. The dev server printed +> its URL to the terminal minutes ago, so I need to search terminal content. + +The agent calls {tool}`search-panes` with `pattern: "Local:"` and +`session_name: "cv"`. The response comes back with pane `%5` in the `react` +window, matched line: `Local: http://localhost:5173/`. + +### Decide + +> The server is alive and its URL is known. I do not need to start anything. +> I just need an idle pane for running tests. + +The agent calls {tool}`list-panes` on the `cv` session. Several panes show +`pane_current_command: zsh` -- idle shells. It picks `%4` in the same window. -```json -{"tool": "capture_pane", "arguments": {"pane_id": "%0", "start": -50}} +### Act + +The agent calls {tool}`send-keys` in pane `%4`: +`PLAYWRIGHT_BASE_URL=http://localhost:5173 pnpm exec playwright test` + +Then it calls {tool}`wait-for-text` on pane `%4` with `pattern: "passed|failed|timed out"`, `regex: true`, and `timeout: 120`. Once the +wait resolves, it calls {tool}`capture-pane` on `%4` with `start: -80` to +read the test results. + +```{tip} +The agent's first instinct might be to *start* a Vite server. But +{tool}`search-panes` reveals one is already running. This avoids a port +conflict, a wasted pane, and the most common agent mistake: treating tmux +like a blank shell. ``` -**Why each step matters:** -- `send_keys` sends the command but returns immediately — it does not wait for completion -- `wait_for_text` blocks until the shell prompt returns (the `$` regex), confirming the command finished -- `capture_pane` reads the result after the command has completed +### The non-obvious part + +{toolref}`search-panes` searches terminal *content* -- what you would see on +screen. {toolref}`list-panes` searches *metadata* like current command and +working directory. If the agent had used {toolref}`list-panes` to find a pane +running `node`, it would know a process exists but not whether it is ready or +what URL it chose. + +**Prompt:** "Run the Playwright tests against my dev server in the cv +session." + +--- + +## 2. Start a service and wait for it before running dependent work + +**Situation.** The developer is starting fresh in their `backend` session -- +no server running yet. They want to run integration tests, but the test +suite needs a live API server. + +### Discover + +> First I need to know what exists in the `backend` session. If a server is +> already running, I should reuse it instead of starting a duplicate. + +The agent calls {tool}`list-panes` for the `backend` session. No pane is +running a server process. A {tool}`search-panes` call for `"listening"` +returns no matches. + +### Decide + +> Nothing to reuse. I need a dedicated pane for the server so its output +> stays separate from the test output. + +### Act + +The agent calls {tool}`split-window` with `session_name: "backend"` to +create a new pane, then calls {tool}`send-keys` in that pane: +`npm run serve`. + +The agent calls {tool}`wait-for-text` on the server pane with +`pattern: "Listening on"` and `timeout: 30`. Once the wait resolves, the +agent calls {tool}`send-keys` in the original pane: +`npm test -- --integration`, then {tool}`wait-for-text` with +`pattern: "passed|failed|error"` and `regex: true`, then +{tool}`capture-pane` to read the test results. ```{warning} -Do not call `capture_pane` immediately after `send_keys`. There is a race condition — you may capture the terminal *before* the command produces output. Always use `wait_for_text` between them. +Calling {toolref}`capture-pane` immediately after {toolref}`send-keys` is a +race condition. {toolref}`send-keys` returns the moment tmux accepts the +keystrokes, not when the command finishes. Always use {toolref}`wait-for-text` +between them. ``` -## Start a service and wait for readiness +### The non-obvious part -Use when you need a background service running before proceeding — web servers, databases, build watchers. +{toolref}`wait-for-text` replaces `sleep`. The server might start in 2 +seconds or 20 -- the agent adapts. The anti-pattern is polling with repeated +{toolref}`capture-pane` calls or hardcoding a sleep duration. The MCP server +handles the polling internally with configurable `timeout` (default 8s) and +`interval` (default 50ms). -```json -{"tool": "split_window", "arguments": {"session_name": "dev", "direction": "right"}} -``` +**Prompt:** "Start the API server in my backend session and run the +integration tests once it's ready." -The new pane's `pane_id` is in the response. Use it for the remaining steps: +--- -```json -{"tool": "send_keys", "arguments": {"keys": "npm run dev", "pane_id": "%1"}} -``` +## 3. Find the failing pane without opening random terminals -```json -{"tool": "wait_for_text", "arguments": {"pattern": "Local:.*http://localhost", "pane_id": "%1", "regex": true, "timeout": 30}} -``` +**Situation.** The developer kicked off multiple jobs across panes in a `ci` +session -- linting, unit tests, integration tests, type checking. One of +them failed, but they stepped away and do not remember which pane. -Now the server is ready — run tests in the original pane: +### Discover -```json -{"tool": "send_keys", "arguments": {"keys": "npx playwright test", "pane_id": "%0"}} -``` +> I should not capture every pane and read them all -- that is expensive and +> slow. Instead I will search for common failure indicators across all panes +> at once. -**Common mistake:** Using a fixed `sleep` instead of `wait_for_text`. Server startup times vary — `wait_for_text` adapts automatically. +The agent calls {tool}`search-panes` with +`pattern: "FAIL|ERROR|error:|Traceback"`, `regex: true`, scoped to +`session_name: "ci"`. -## Search for errors across all panes +### Decide -Find which pane has an error without knowing where to look. +> Two panes matched: `%3` has `FAIL: test_upload` and `%6` has +> `error: Type 'string' is not assignable`. I will capture context from each. -```json -{"tool": "search_panes", "arguments": {"pattern": "error", "session_name": "dev"}} -``` +### Act -The response lists every pane with matching lines. Then capture the full context from each match: +The agent calls {tool}`capture-pane` on `%3` with `start: -60`, then on +`%6` with `start: -60`. -```json -{"tool": "capture_pane", "arguments": {"pane_id": "%2", "start": -100}} +```{tip} +If the error scrolled off the visible screen, use `content_start: -200` (or +deeper) when calling {tool}`search-panes`. The `content_start` parameter +makes search reach into scrollback history, not just the visible screen. ``` -**Why two steps:** `search_panes` is fast — it uses tmux's built-in filter for plain text patterns and never captures full pane content. Once you know *which* pane has the error, `capture_pane` gets the full context. +### The non-obvious part -**Common mistake:** Using `list_panes` to find errors. `list_panes` only searches metadata (names, IDs, current command) — not terminal content. +{toolref}`search-panes` checks all panes in a single call -- searching 20 +panes costs roughly the same as searching 2. An agent that instead calls +{toolref}`list-panes` then {toolref}`capture-pane` on each one individually +makes 20+ round trips for the same information. The `regex: true` parameter +is required here because the `|` in the pattern is a regex alternation, not +literal text. -## Set up a multi-pane workspace +**Prompt:** "Check my ci session -- which jobs failed?" -Create a structured development layout with labeled panes. +--- -```json -{"tool": "create_session", "arguments": {"session_name": "workspace"}} -``` +## 4. Interrupt a stuck process and recover the pane -```json -{"tool": "split_window", "arguments": {"session_name": "workspace", "direction": "right"}} -``` +**Situation.** A long-running build is hanging. The developer wants to +interrupt it, verify the pane is responsive, and re-run the command. -```json -{"tool": "split_window", "arguments": {"session_name": "workspace", "direction": "below"}} -``` +### Discover -```json -{"tool": "select_layout", "arguments": {"session_name": "workspace", "layout": "main-vertical"}} -``` +> I need to send Ctrl-C. This is a tmux key name, not text -- so I must use +> `enter: false` or tmux will send Ctrl-C followed by Enter, which could +> confirm a prompt I did not intend to answer. -Label each pane for later identification: +The agent calls {tool}`send-keys` with `keys: "C-c"` and `enter: false` on +the target pane. -```json -{"tool": "set_pane_title", "arguments": {"pane_id": "%0", "title": "editor"}} -``` +### Decide -```json -{"tool": "set_pane_title", "arguments": {"pane_id": "%1", "title": "server"}} -``` +> Did the interrupt work? Some processes ignore SIGINT. I will wait briefly +> for a shell prompt to reappear. Developers use custom prompts, so I cannot +> just look for `$`. -```json -{"tool": "set_pane_title", "arguments": {"pane_id": "%2", "title": "tests"}} -``` +The agent calls {tool}`wait-for-text` with `pattern: "[$#>%] *$"`, +`regex: true`, and `timeout: 5`. -Then start processes in each: +> If the wait resolves, the shell is back. If it times out, the process +> ignored Ctrl-C. I will escalate: try SIGQUIT (`C-\` with `enter: false`), +> then destroy and replace the pane only as a last resort. -```json -{"tool": "send_keys", "arguments": {"keys": "vim .", "pane_id": "%0"}} -``` +### Act + +If the wait times out, the agent sends `C-\` (also with `enter: false`). If +that also fails, it calls {tool}`kill-pane` on the stuck pane, then +{tool}`split-window` to create a replacement, then {tool}`send-keys` to +re-run. -```json -{"tool": "send_keys", "arguments": {"keys": "npm run dev", "pane_id": "%1"}} +```{warning} +The `enter: false` parameter is critical. Without it, {toolref}`send-keys` +sends Ctrl-C *then* Enter, which could confirm a "really quit?" prompt, +submit a partially typed command, or enter a newline into a REPL. ``` -## Interrupt a hung command +### The non-obvious part -Recover when a command is stuck or waiting for input. +Recovery is a two-step decision. Try the gentle approach first (Ctrl-C), +verify it worked with {toolref}`wait-for-text`, escalate only if needed. The +escalation ladder is: interrupt, verify, escalate signal, destroy. Skipping +straight to {toolref}`kill-pane` loses the pane's scrollback history and any +partially written output that might explain *why* it hung. -```json -{"tool": "send_keys", "arguments": {"keys": "C-c", "pane_id": "%0", "enter": false}} -``` +**Prompt:** "The build in pane %2 is stuck. Kill it and restart." -```json -{"tool": "wait_for_text", "arguments": {"pattern": "\\$\\s*$", "pane_id": "%0", "regex": true, "timeout": 5}} -``` +--- -**Why `enter: false`:** Ctrl-C is a tmux key name, not text to type. Setting `enter: false` prevents sending an extra Enter keystroke after the interrupt signal. +## 5. Re-run a command without mixing old and new output -**If the interrupt fails** (process ignores Ctrl-C), use `kill_pane` to destroy the pane and `split_window` to get a fresh one. +**Situation.** The developer wants `pytest` re-run in tmux, but the +candidate pane already has old test output in scrollback. They want only +fresh results. -## Clean capture (no prior output) +### Discover -Get a clean capture without output from previous commands. +The agent calls {tool}`list-panes` to find the pane by title, cwd, or +current command. If more than one pane is plausible, it uses +{tool}`capture-pane` with a small range to confirm the target. -```json -{"tool": "clear_pane", "arguments": {"pane_id": "%0"}} -``` +### Decide -```json -{"tool": "send_keys", "arguments": {"keys": "pytest -x", "pane_id": "%0"}} -``` +> The pane is a shell. I should clear it before running so the capture +> afterwards contains only fresh output. If it were running a watcher or +> long-lived process, I would not hijack it -- I would use a different pane. -```json -{"tool": "wait_for_text", "arguments": {"pattern": "passed|failed|error", "pane_id": "%0", "regex": true, "timeout": 60}} -``` +### Act + +The agent calls {tool}`clear-pane`, then {tool}`send-keys` with +`keys: "pytest"`, then {tool}`wait-for-text` with +`pattern: "passed|failed|error"` and `regex: true`, then +{tool}`capture-pane` to read the fresh output. + +### The non-obvious part + +{toolref}`clear-pane` runs two tmux commands internally (`send-keys -R` then +`clear-history`) with a brief gap between them. Calling +{toolref}`capture-pane` immediately after {toolref}`clear-pane` may catch +partial state. The {toolref}`wait-for-text` call after {toolref}`send-keys` +naturally provides the needed delay, so the sequence clear-send-wait-capture +is safe. + +**Prompt:** "Run `pytest` in the test pane and show me only the fresh +output." + +--- + +## 6. Build a workspace the agent can revisit later -```json -{"tool": "capture_pane", "arguments": {"pane_id": "%0"}} +**Situation.** The developer wants a durable project workspace -- not just a +quick split, but a layout that later prompts can refer to by role ("the +server pane", "the test pane"). + +### Discover + +> Before creating anything, I need to check whether a session with this name +> already exists. Creating a duplicate will fail. + +The agent calls {tool}`list-sessions`. No session named `myproject` exists. + +### Decide + +> Safe to create. I need three panes: editor, server, tests. I will create +> the session, split twice, then apply a layout so tmux handles the geometry +> instead of me calculating sizes. + +### Act + +The agent calls {tool}`create-session` with `session_name: "myproject"` and +`start_directory: "/home/dev/myproject"`. Then {tool}`split-window` twice +(with `direction: "right"` and `direction: "below"`), followed by +{tool}`select-layout` with `layout: "main-vertical"`. + +The agent calls {tool}`set-pane-title` on each pane: `editor`, `server`, +`tests`. + +The agent calls {tool}`send-keys` in the server pane: `npm run dev`, then +{tool}`wait-for-text` for `pattern: "ready|listening|Local:"` with +`regex: true` and `timeout: 30`. + +```{tip} +If the session *does* already exist, the right move is to reuse and extend +it, not recreate it. The {toolref}`list-sessions` check at the top is what +makes that decision possible. ``` -**Why clear first:** Without clearing, `capture_pane` returns the visible viewport which may include output from prior commands. Clearing ensures you only capture output from the command you just ran. +### The non-obvious part + +Titles and naming are not cosmetic. They reduce future discovery cost. When +the agent comes back in a later conversation and the user says "restart the +server," the agent calls {toolref}`list-panes`, finds the pane titled +`server`, and acts -- no searching, no guessing, no capturing every pane to +figure out which one is which. But note: pane IDs are ephemeral across tmux +server restarts, so the agent should always re-discover by metadata (session +name, pane title, cwd) rather than trusting remembered `%N` values. + +**Prompt:** "Set up a tmux workspace for myproject with editor, server, and +test panes." + +--- + +## What to read next + +For the principles that recur across these recipes -- discover before acting, +wait instead of polling, content vs. metadata, prefer IDs, escalate +gracefully -- see the {ref}`prompting guide `. For specific +pitfalls like `enter: false` and the `send_keys`/`capture_pane` race +condition, see {ref}`gotchas `. + From 90fe9c94054542125a6802c45d387bbba7bb1586 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 18:51:53 -0500 Subject: [PATCH 13/19] docs(recipes): Drop numbered prefixes from recipe titles why: Titles read cleaner without ordinal numbers. The page order already implies sequence. --- docs/recipes.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 249f808..bb71300 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -19,7 +19,7 @@ Every recipe uses the same structure: --- -## 1. Find a running dev server and test against it +## Find a running dev server and test against it **Situation.** A developer manages a CV project with tmuxp. One pane is already running `pnpm start` with Vite somewhere in the `react` window. @@ -73,7 +73,7 @@ session." --- -## 2. Start a service and wait for it before running dependent work +## Start a service and wait for it before running dependent work **Situation.** The developer is starting fresh in their `backend` session -- no server running yet. They want to run integration tests, but the test @@ -126,7 +126,7 @@ integration tests once it's ready." --- -## 3. Find the failing pane without opening random terminals +## Find the failing pane without opening random terminals **Situation.** The developer kicked off multiple jobs across panes in a `ci` session -- linting, unit tests, integration tests, type checking. One of @@ -171,7 +171,7 @@ literal text. --- -## 4. Interrupt a stuck process and recover the pane +## Interrupt a stuck process and recover the pane **Situation.** A long-running build is hanging. The developer wants to interrupt it, verify the pane is responsive, and re-run the command. @@ -223,7 +223,7 @@ partially written output that might explain *why* it hung. --- -## 5. Re-run a command without mixing old and new output +## Re-run a command without mixing old and new output **Situation.** The developer wants `pytest` re-run in tmux, but the candidate pane already has old test output in scrollback. They want only @@ -262,7 +262,7 @@ output." --- -## 6. Build a workspace the agent can revisit later +## Build a workspace the agent can revisit later **Situation.** The developer wants a durable project workspace -- not just a quick split, but a layout that later prompts can refer to by role ("the From dd2e7f625e216c0e9dfa55806fac942d546d6019 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 18:53:59 -0500 Subject: [PATCH 14/19] docs(recipes): Link tmuxp, generalize CV project to React MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: tmuxp should link to its docs. "CV project" is too specific — "React project" is more relatable to the general audience. what: - Link tmuxp to https://tmuxp.git-pull.com - "CV project" → "React project" - session_name "cv" → "myapp" --- docs/recipes.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index bb71300..d2fd79f 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -21,10 +21,11 @@ Every recipe uses the same structure: ## Find a running dev server and test against it -**Situation.** A developer manages a CV project with tmuxp. One pane is -already running `pnpm start` with Vite somewhere in the `react` window. -They want to run Playwright e2e tests. The agent does not know which pane -has the server, or what port it chose. +**Situation.** A developer manages a React project with +[tmuxp](https://tmuxp.git-pull.com). One pane is already running +`pnpm start` with Vite somewhere in the `react` window. They want to run +Playwright e2e tests. The agent does not know which pane has the server, +or what port it chose. ### Discover @@ -33,7 +34,7 @@ has the server, or what port it chose. > its URL to the terminal minutes ago, so I need to search terminal content. The agent calls {tool}`search-panes` with `pattern: "Local:"` and -`session_name: "cv"`. The response comes back with pane `%5` in the `react` +`session_name: "myapp"`. The response comes back with pane `%5` in the `react` window, matched line: `Local: http://localhost:5173/`. ### Decide @@ -41,7 +42,7 @@ window, matched line: `Local: http://localhost:5173/`. > The server is alive and its URL is known. I do not need to start anything. > I just need an idle pane for running tests. -The agent calls {tool}`list-panes` on the `cv` session. Several panes show +The agent calls {tool}`list-panes` on the `myapp` session. Several panes show `pane_current_command: zsh` -- idle shells. It picks `%4` in the same window. ### Act @@ -68,7 +69,7 @@ working directory. If the agent had used {toolref}`list-panes` to find a pane running `node`, it would know a process exists but not whether it is ready or what URL it chose. -**Prompt:** "Run the Playwright tests against my dev server in the cv +**Prompt:** "Run the Playwright tests against my dev server in the myapp session." --- From 975f1fe28518554cecffb21463f8c3965cebedfb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 18:59:03 -0500 Subject: [PATCH 15/19] docs(glossary): Add SIGINT and SIGQUIT terms, link from recipes why: The interrupt recipe references SIGINT and SIGQUIT without explaining them. Glossary terms with {term} cross-references let readers hover or click for definitions. what: - Add SIGINT and SIGQUIT to docs/glossary.md with send_keys usage - Link 4 occurrences in docs/recipes.md via {term} role --- docs/glossary.md | 6 ++++++ docs/recipes.md | 11 ++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/glossary.md b/docs/glossary.md index 11dea8d..b5219ac 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -33,4 +33,10 @@ Safety tier Socket The Unix socket used to communicate with a tmux server. Can be specified by name (`-L`) or path (`-S`). + +SIGINT + Interrupt signal (Ctrl-C). Sent via {ref}`send-keys` with `keys: "C-c"` and `enter: false`. Most processes terminate gracefully on SIGINT. + +SIGQUIT + Quit signal (Ctrl-\\). Sent via {ref}`send-keys` with `keys: "C-\\"` and `enter: false`. Stronger than {term}`SIGINT` — may produce a core dump on Unix. Use as an escalation when SIGINT is ignored. ``` diff --git a/docs/recipes.md b/docs/recipes.md index d2fd79f..c40ad61 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -188,7 +188,7 @@ the target pane. ### Decide -> Did the interrupt work? Some processes ignore SIGINT. I will wait briefly +> Did the interrupt work? Some processes ignore {term}`SIGINT`. I will wait briefly > for a shell prompt to reappear. Developers use custom prompts, so I cannot > just look for `$`. @@ -196,7 +196,7 @@ The agent calls {tool}`wait-for-text` with `pattern: "[$#>%] *$"`, `regex: true`, and `timeout: 5`. > If the wait resolves, the shell is back. If it times out, the process -> ignored Ctrl-C. I will escalate: try SIGQUIT (`C-\` with `enter: false`), +> ignored Ctrl-C. I will escalate: try {term}`SIGQUIT` (`C-\` with `enter: false`), > then destroy and replace the pane only as a last resort. ### Act @@ -214,9 +214,10 @@ submit a partially typed command, or enter a newline into a REPL. ### The non-obvious part -Recovery is a two-step decision. Try the gentle approach first (Ctrl-C), -verify it worked with {toolref}`wait-for-text`, escalate only if needed. The -escalation ladder is: interrupt, verify, escalate signal, destroy. Skipping +Recovery is a two-step decision. Try {term}`SIGINT` first (Ctrl-C), +verify it worked with {toolref}`wait-for-text`, escalate to {term}`SIGQUIT` +only if needed. The escalation ladder is: interrupt, verify, escalate signal, +destroy. Skipping straight to {toolref}`kill-pane` loses the pane's scrollback history and any partially written output that might explain *why* it hung. From 521d2aa220e4f7afd1e01bc395f0af4dcbfab15e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 19:00:47 -0500 Subject: [PATCH 16/19] docs: Add orphaned demo page showcasing badge system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Provides a single-page reference for all custom roles, badges, and visual elements — useful for contributors and design review. what: - Orphaned page at /demo/ (not in any toctree) - Showcases: {badge}, {tool}, {toolref}, {ref}, {envvar}, {term} - Shows badges in headings, tables, prose, and dense inline context - Documents badge HTML anatomy and accessibility features --- docs/demo.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/demo.md diff --git a/docs/demo.md b/docs/demo.md new file mode 100644 index 0000000..3b43dfc --- /dev/null +++ b/docs/demo.md @@ -0,0 +1,100 @@ +--- +orphan: true +--- + +# Badge & Role Demo + +A showcase of the custom Sphinx roles and visual elements available in libtmux-mcp documentation. + +## Safety badges + +Standalone badges via `{badge}`: + +- {badge}`readonly` — green, read-only operations +- {badge}`mutating` — amber, state-changing operations +- {badge}`destructive` — red, irreversible operations + +## Tool references + +### `{tool}` — code-linked with badge + +{tool}`capture-pane` · {tool}`send-keys` · {tool}`search-panes` · {tool}`wait-for-text` · {tool}`kill-pane` · {tool}`create-session` · {tool}`split-window` + +### `{toolref}` — code-linked, no badge + +{toolref}`capture-pane` · {toolref}`send-keys` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`kill-pane` · {toolref}`create-session` · {toolref}`split-window` + +### `{ref}` — plain text link + +{ref}`capture-pane` · {ref}`send-keys` · {ref}`search-panes` · {ref}`wait-for-text` · {ref}`kill-pane` · {ref}`create-session` · {ref}`split-window` + +## Badges in context + +### In a heading + +These are the actual tool headings as they render on tool pages: + +> `capture_pane` {badge}`readonly` + +> `split_window` {badge}`mutating` + +> `kill_session` {badge}`destructive` + +### In a table + +| Tool | Tier | Description | +|------|------|-------------| +| {toolref}`list-sessions` | {badge}`readonly` | List all sessions | +| {toolref}`send-keys` | {badge}`mutating` | Send commands to a pane | +| {toolref}`kill-pane` | {badge}`destructive` | Destroy a pane | + +### In prose + +Use {tool}`search-panes` to find text across all panes. If you know which pane, use {tool}`capture-pane` instead. After running a command with {tool}`send-keys`, always {tool}`wait-for-text` before capturing. + +### Dense inline (toolref, no badges) + +The fundamental pattern: {toolref}`send-keys` → {toolref}`wait-for-text` → {toolref}`capture-pane`. For discovery: {toolref}`list-sessions` → {toolref}`list-panes` → {toolref}`get-pane-info`. + +## Environment variable references + +{envvar}`LIBTMUX_SOCKET` · {envvar}`LIBTMUX_SAFETY` · {envvar}`LIBTMUX_SOCKET_PATH` · {envvar}`LIBTMUX_TMUX_BIN` + +## Glossary terms + +{term}`SIGINT` · {term}`SIGQUIT` · {term}`MCP` · {term}`Safety tier` · {term}`Pane` · {term}`Session` + +## Admonitions + +```{tip} +Use {tool}`search-panes` before {tool}`capture-pane` when you don't know which pane has the output you need. +``` + +```{warning} +Do not call {toolref}`capture-pane` immediately after {toolref}`send-keys` — there is a race condition. Use {toolref}`wait-for-text` between them. +``` + +```{note} +All tools accept an optional `socket_name` parameter for multi-server support. +``` + +## Badge anatomy + +Each badge renders as: + +```html + + 🔍 readonly + +``` + +Features: +- **Emoji icon** — 🔍 readonly, ✏️ mutating, 💣 destructive (native system emoji, no filters) +- **Matte colors** — forest green, smoky amber, matte crimson with 1px border +- **Accessible** — `role="note"` + `aria-label` for screen readers +- **Non-selectable** — `user-select: none` so copying tool names skips badge text +- **Context-aware sizing** — slightly larger in headings, smaller inline +- **Sidebar compression** — badges collapse to colored dots in the right-side TOC +- **Heading flex** — `h2/h3/h4:has(.sd-badge)` centers badge against cap-height From b48987dc3df8a0b07dcdbe64be1892fd128a19a9 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 19:02:20 -0500 Subject: [PATCH 17/19] docs(glossary): Use {toolref} for send_keys in SIGINT/SIGQUIT definitions why: {ref} renders as plain text. {toolref} renders as code-formatted linked text, matching how tool names appear elsewhere in the docs. --- docs/glossary.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/glossary.md b/docs/glossary.md index b5219ac..df7c39c 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -35,8 +35,8 @@ Socket The Unix socket used to communicate with a tmux server. Can be specified by name (`-L`) or path (`-S`). SIGINT - Interrupt signal (Ctrl-C). Sent via {ref}`send-keys` with `keys: "C-c"` and `enter: false`. Most processes terminate gracefully on SIGINT. + Interrupt signal (Ctrl-C). Sent via {toolref}`send-keys` with `keys: "C-c"` and `enter: false`. Most processes terminate gracefully on SIGINT. SIGQUIT - Quit signal (Ctrl-\\). Sent via {ref}`send-keys` with `keys: "C-\\"` and `enter: false`. Stronger than {term}`SIGINT` — may produce a core dump on Unix. Use as an escalation when SIGINT is ignored. + Quit signal (Ctrl-\\). Sent via {toolref}`send-keys` with `keys: "C-\\"` and `enter: false`. Stronger than {term}`SIGINT` — may produce a core dump on Unix. Use as an escalation when SIGINT is ignored. ``` From 43987a4af1c0d8d8e6c972a353f74b21d040f789 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 19:09:45 -0500 Subject: [PATCH 18/19] style(docs): Vertically align badge with code text in {tool} links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Badge sat slightly higher/lower than the adjacent code element inside {tool} cross-reference links. what: - Add vertical-align: middle to both .sd-badge and code inside a.reference:has(.sd-badge) - No inline-flex on the — avoids inflating line box height --- docs/_static/css/custom.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 66b9708..13bdf19 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -332,6 +332,11 @@ code.docutils + .sd-badge, /* ── Link behavior: underline code only, on hover ───────── */ a.reference .sd-badge { text-decoration: none; + vertical-align: middle; +} + +a.reference:has(.sd-badge) code { + vertical-align: middle; } a.reference:has(.sd-badge) { From d78392bf732deddafe64f4166c56c0c0f2509a3d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 19:10:56 -0500 Subject: [PATCH 19/19] style(docs): Add vertical-align middle to base badge rule why: Standalone {badge} in prose text was misaligned with surrounding text. The heading flex and link rules handled their contexts, but the base rule had no vertical-align. --- docs/_static/css/custom.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 13bdf19..874e40d 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -258,6 +258,7 @@ h4:has(> .sd-badge) { .sd-badge { display: inline-flex !important; align-items: center; + vertical-align: middle; gap: 0.28rem; font-size: 0.67rem; font-weight: 650;