Skip to content

Commit 6534251

Browse files
authored
feat: respawn_pane, get_*_info reads, mcp_swap dev script (#27)
Three follow-up tracks ship together: a destructive-but-recoverable pane lifecycle tool, single-object info reads that complete the get_*_info hierarchy, and a cross-CLI dev script that points Claude / Codex / Cursor / Gemini at a local checkout in one command. Server instructions, audit redaction, and the safety topic are extended in lockstep. **New tools** - **`respawn_pane`** — restart a wedged pane in place, preserving `pane_id` and the window layout where the kill-and-resplit alternative would invalidate references and rearrange the layout. Refuses to respawn the MCP server's own pane (mirroring the existing `kill_*` self-guards), requires explicit `pane_id` (no hierarchical fallback), and carries `destructiveHint=true` / `idempotentHint=false` annotations while staying in the `mutating` tier so default-profile agents can still reach it for shell recovery. - **`get_session_info`**, **`get_window_info`** — targeted single-object metadata reads, completing the four-level `get_*_info` hierarchy alongside `get_server_info` / `get_pane_info`. Buffers, hooks, and options have existing read paths and are intentionally excluded. **New dev script: `scripts/mcp_swap.py`** - One command (`just mcp-use-local`) rewrites each detected CLI's global config to run the local repo via `uv`; `just mcp-revert` restores from a timestamped backup. `just mcp-detect` and `just mcp-status` report install state. - Atomic writes (tempfile + `os.replace`) with re-validation, layout-shape guard before mutating Claude's per-project config, per-CLI state file at `$XDG_STATE_HOME/libtmux-mcp-dev/swap/`, env preservation on replacement, `--dry-run` mode that prints a unified diff. Scope is global configs only — workspace configs remain the CLI's native responsibility (`cursor mcp add`, `gemini mcp add`). **Audit redaction** `shell` (string) and `environment` (dict) on `respawn_pane` join the existing redaction set. Dict-shaped values keep their *keys* visible (env var names like `DATABASE_URL` are operator-debug-useful) but digest each *value* via `{len, sha256_prefix}`. Documented limitation: values may briefly appear in the OS process table while tmux spawns the new process — the audit log redaction does not cover that window. **LLM discoverability and server instructions** - `display_message` retitled to "Evaluate tmux Format String" with a docstring leading with read-only format expansion. Eliminates the naming clash with tmux's `display-message` verb that pushed agents toward the wrong affordance. - `pipe_pane` docstring leads with the concrete `output_path="/tmp/pane.log"` logging use case. - `_BASE_INSTRUCTIONS` decomposed into named segments with module-level guidance on when to add a new gap-explainer vs. push the explanation into a tool docstring. New segments: hooks are read-only by design (`HOOKS ARE READ-ONLY`), buffer lifecycle plus why `list_buffers` is intentionally absent, `is_caller` workflow inside tmux, qualified `socket_name` contract that names `list_servers` as the documented exception. A contract test enforces that every registered tool except `list_servers` accepts `socket_name` so prose and signatures cannot drift. **Documentation** - Topic pages added for `respawn-pane`, `get-session-info`, and `get-window-info`. - `safety.md` adds a `respawn-pane` Footgun section alongside `pipe-pane` and `set-environment`. The macOS `TMUX_TMPDIR` caveat is rewritten to reflect shipped behaviour — tmux's three-step socket-path resolution is documented, so operators no longer need to set `TMUX_TMPDIR` explicitly to chase a problem that's already solved. - README tool index updated for the three new tools. **Pre-release decisions (no migration path needed for users)** - `respawn_pane` parameter renamed `shell_command` → `shell` to align with `split_window(shell=…)` and the upstream `Pane.respawn(shell=…)` API. - `respawn_pane` drops the `session_id` / `session_name` / `window_id` resolver fallbacks. The runtime guard rejected any call missing `pane_id` anyway; validation now lives at the FastMCP schema boundary. **Stopgap noted in source** `respawn_pane` emits a literal `pane.cmd("respawn-pane", *argv)` because libtmux 0.55.x has no `Pane.respawn()` yet. The argv shape mirrors the upstream `tmux-parity` branch: `-k`, `-c <dir>`, repeated `-eKEY=VALUE` (joined form), then optional trailing shell. When the libtmux release line picks it up, swap to `pane.respawn(...)` and drop the stderr branch. **State path** `scripts/mcp_swap.py` writes to `$XDG_STATE_HOME/libtmux-mcp-dev/swap/state.json` (default `~/.local/state/libtmux-mcp-dev/swap/state.json`). The `-dev` namespace makes the dev-tooling status loud and avoids the collision with the runtime package name. No migration shim — pre-alpha, dev-only, single-user surface.
2 parents f0c8c07 + d8f0098 commit 6534251

38 files changed

Lines changed: 2411 additions & 68 deletions

CHANGES

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@
66
_Notes on upcoming releases will be added here_
77
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->
88

9+
### What's new
10+
11+
#### New tool: `respawn_pane`
12+
13+
Restart a wedged pane in place — preserves `pane_id` and the window layout (the alternative `kill_pane` + `split_window` invalidates pane references and rearranges the window). Required `pane_id`; optional `kill` (default true), `shell`, `start_directory`, `environment`. Refuses to respawn the MCP server's own pane, mirroring the existing `kill_*` self-guards. Annotations honestly mark it `destructiveHint=true` / `idempotentHint=false` while staying in the `mutating` tier so default-profile agents can use it for shell recovery. Audit log redacts the `shell` argument and digests `environment` values (keys stay visible). (#27)
14+
15+
#### New tools: `get_session_info`, `get_window_info`
16+
17+
Targeted single-object metadata reads — no more `list_*` + filter when you have an ID. Completes the four-level `get_*_info` hierarchy (server / session / window / pane); buffers, hooks, and options have existing read paths and are deliberately excluded. (#27)
18+
19+
#### New dev script: `scripts/mcp_swap.py`
20+
21+
Point Claude / Codex / Cursor / Gemini at a local checkout in one command. `just mcp-use-local` rewrites each CLI's global config to run the local repo via `uv`; `just mcp-revert` restores from a timestamped backup. `just mcp-detect` and `just mcp-status` report install state. Atomic writes, `--dry-run` mode, per-CLI state file, env preservation on replacement, layout-shape guard before mutating Claude's per-project config. Scope is global configs only — see `scripts/README.md`. (#27)
22+
23+
### Documentation
24+
25+
- {ref}`safety` adds a {tooliconl}`respawn-pane` "Footgun" subsection alongside {tooliconl}`pipe-pane` and {tooliconl}`set-environment`: `kill=true` default, non-idempotent retries, explicit-`pane_id` requirement, OS-process-table visibility for `shell` / `environment`. The macOS `TMUX_TMPDIR` caveat is rewritten to reflect shipped behaviour — tmux's three-step socket-path resolution is documented, so operators no longer need to set `TMUX_TMPDIR` explicitly to chase a problem that's already solved. (#27)
26+
- LLM-facing discoverability tightened: {tooliconl}`display-message` retitled "Evaluate tmux Format String" with a docstring that leads with read-only format expansion (the tool wraps `display-message -p` but `-p` expands rather than displays), {tooliconl}`pipe-pane` leads with the `/tmp/pane.log` logging use case, and the server instructions explain why hooks are read-only and why there is no `list_buffers` tool. The `socket_name` contract is tightened to acknowledge {tooliconl}`list-servers` as the documented exception. (#27)
27+
- Topic pages added for {tooliconl}`respawn-pane`, {tooliconl}`get-session-info`, and {tooliconl}`get-window-info`. (#27)
28+
29+
### API decisions (pre-release)
30+
31+
- {tooliconl}`respawn-pane` settles on `shell` (renamed from `shell_command`) to align with {tooliconl}`split-window` and upstream `Pane.respawn(shell=)`, and drops the `session_name` / `session_id` / `window_id` resolver fallbacks — the runtime guard rejected any call missing `pane_id` anyway. Validation now lives at the FastMCP schema boundary. (#27)
32+
933
## libtmux-mcp 0.1.0a3 (2026-04-19)
1034

1135
_Post-0.1.0a2 smoke-test fixes and `libtmux` floor bump_

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ Give your AI agent hands inside the terminal — create sessions, run commands,
1616
| Module | Tools |
1717
|--------|-------|
1818
| **Server** | `list_sessions`, `create_session`, `kill_server`, `get_server_info` |
19-
| **Session** | `list_windows`, `create_window`, `rename_session`, `select_window`, `kill_session` |
20-
| **Window** | `list_panes`, `split_window`, `rename_window`, `select_layout`, `resize_window`, `move_window`, `kill_window` |
21-
| **Pane** | `send_keys`, `paste_text`, `capture_pane`, `snapshot_pane`, `search_panes`, `get_pane_info`, `wait_for_text`, `wait_for_content_change`, `display_message`, `select_pane`, `swap_pane`, `resize_pane`, `set_pane_title`, `clear_pane`, `pipe_pane`, `enter_copy_mode`, `exit_copy_mode`, `kill_pane` |
19+
| **Session** | `list_windows`, `get_session_info`, `create_window`, `rename_session`, `select_window`, `kill_session` |
20+
| **Window** | `list_panes`, `get_window_info`, `split_window`, `rename_window`, `select_layout`, `resize_window`, `move_window`, `kill_window` |
21+
| **Pane** | `send_keys`, `paste_text`, `capture_pane`, `snapshot_pane`, `search_panes`, `get_pane_info`, `wait_for_text`, `wait_for_content_change`, `display_message`, `select_pane`, `swap_pane`, `resize_pane`, `set_pane_title`, `clear_pane`, `pipe_pane`, `enter_copy_mode`, `exit_copy_mode`, `respawn_pane`, `kill_pane` |
2222
| **Options** | `show_option`, `set_option` |
2323
| **Environment** | `show_environment`, `set_environment` |
2424

docs/_ext/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Sphinx extensions bundled with the project documentation."""
2+
3+
from __future__ import annotations

docs/_widgets/mcp-install/widget.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
MCPInstallWidget.context() / the directive's option merge.
55

66
Each code block runs through the `highlight` filter (defined in
7-
widgets._base.make_highlight_filter) which wraps Sphinx's PygmentsBridge —
7+
docs._ext.widgets._base.make_highlight_filter) which wraps Sphinx's
8+
PygmentsBridge —
89
so the output is byte-identical to a native ``.. code-block::`` block,
910
meaning sphinx-copybutton + its prompt-strip regex work automatically.
1011
#}

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
project_root = cwd.parent
2020
project_src = project_root / "src"
2121

22+
sys.path.insert(0, str(project_root))
2223
sys.path.insert(0, str(project_src))
23-
sys.path.insert(0, str(cwd / "_ext"))
2424

2525
# package data
2626
about: dict[str, str] = {}
@@ -40,7 +40,7 @@
4040
"sphinx_autodoc_api_style",
4141
"sphinx.ext.todo",
4242
"sphinx_autodoc_fastmcp",
43-
"widgets",
43+
"docs._ext.widgets",
4444
],
4545
intersphinx_mapping={
4646
"python": ("https://docs.python.org/", None),

docs/tools/pane/display-message.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
# Display message
1+
# Evaluate tmux format string (display_message)
22

33
```{fastmcp-tool} pane_tools.display_message
44
```
55

66
**Use when** you need to query arbitrary tmux variables — zoom state, pane
77
dead flag, client activity, or any `#{format}` string that isn't covered by
8-
other tools.
8+
other tools. Despite the historical name (`display_message` is the tmux verb
9+
it wraps), this tool does **not** display anything to the user; it expands
10+
the format string with `display-message -p` and returns the value.
911

1012
**Avoid when** a dedicated tool already provides the information — e.g. use
1113
{tooliconl}`snapshot-pane` for cursor position and mode, or
@@ -33,5 +35,3 @@ zoomed=0 dead=0
3335

3436
```{fastmcp-tool-input} pane_tools.display_message
3537
```
36-
37-
## Act

docs/tools/pane/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ Block until a tmux wait-for channel is signalled.
8181
Signal a waiting channel.
8282
:::
8383

84+
:::{grid-item-card} {tooliconl}`respawn-pane`
85+
Restart a pane's process in place, preserving pane_id.
86+
:::
87+
8488
:::{grid-item-card} {tooliconl}`kill-pane`
8589
Terminate a pane. Destructive.
8690
:::
@@ -110,5 +114,6 @@ wait-for-text
110114
wait-for-content-change
111115
wait-for-channel
112116
signal-channel
117+
respawn-pane
113118
kill-pane
114119
```

docs/tools/pane/respawn-pane.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Respawn pane
2+
3+
```{fastmcp-tool} pane_tools.respawn_pane
4+
```
5+
6+
**Use when** a pane's shell or command has wedged (hung REPL, runaway
7+
process, bad terminal mode) and you need a clean restart *without*
8+
destroying the `pane_id` references other tools or callers may still
9+
be holding. With `kill=True` (the default) tmux kills the current
10+
process first; optional `shell` relaunches with a different command;
11+
optional `start_directory` sets its cwd; optional `environment` adds
12+
per-process env vars (one `-e KEY=VALUE` flag per entry).
13+
14+
**Avoid when** the pane genuinely needs to go away — use
15+
{tooliconl}`kill-pane` instead. Also avoid when you want to change
16+
the layout: `respawn-pane` preserves the pane in place.
17+
18+
**Side effects:** Kills the current process (with `kill=True`) and
19+
starts a new one. **The `pane_id` is preserved** — that's the whole
20+
point of the tool. `pane_pid` updates to the new process.
21+
22+
**Tip:** Call {tooliconl}`get-pane-info` first if you need to capture
23+
`pane_current_command` before respawn — the new process loses its argv.
24+
Omitting `shell` makes tmux replay the original argv (good default for
25+
shells; may differ for processes spawned via custom shell at split
26+
time).
27+
28+
**Example — recover a wedged pane, relaunching the default shell:**
29+
30+
```json
31+
{
32+
"tool": "respawn_pane",
33+
"arguments": {
34+
"pane_id": "%5"
35+
}
36+
}
37+
```
38+
39+
**Example — relaunch with a different command and working directory:**
40+
41+
```json
42+
{
43+
"tool": "respawn_pane",
44+
"arguments": {
45+
"pane_id": "%5",
46+
"shell": "pytest -x",
47+
"start_directory": "/home/user/project"
48+
}
49+
}
50+
```
51+
52+
**Example — relaunch with extra environment variables:**
53+
54+
```json
55+
{
56+
"tool": "respawn_pane",
57+
"arguments": {
58+
"pane_id": "%5",
59+
"shell": "pytest -x",
60+
"environment": {
61+
"PYTHONPATH": "/home/user/project/src",
62+
"DATABASE_URL": "postgres://localhost/test"
63+
}
64+
}
65+
}
66+
```
67+
68+
The audit log redacts each `environment` *value* via `{len, sha256_prefix}` digests but keeps the keys visible (env var names like `DATABASE_URL` are operator-debug-useful, while their values are the secret). Note that values may still appear briefly in the OS process table while tmux spawns the new process — see {ref}`safety` for details.
69+
70+
Response (PaneInfo):
71+
72+
```json
73+
{
74+
"pane_id": "%5",
75+
"pane_pid": "98765",
76+
"pane_current_command": "pytest",
77+
"pane_current_path": "/home/user/project",
78+
...
79+
}
80+
```
81+
82+
```{fastmcp-tool-input} pane_tools.respawn_pane
83+
```
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Get session info
2+
3+
```{fastmcp-tool} session_tools.get_session_info
4+
```
5+
6+
**Use when** you need metadata for a single session (ID, name, window
7+
count, attachment status, activity timestamp) and you already know its
8+
`session_id` or `session_name`. Avoids the `list_sessions` + filter dance.
9+
10+
**Avoid when** you need every session — call `list_sessions` or iterate
11+
via the `tmux://sessions` resource.
12+
13+
**Side effects:** None. Readonly.
14+
15+
**Example:**
16+
17+
```json
18+
{
19+
"tool": "get_session_info",
20+
"arguments": {
21+
"session_id": "$0"
22+
}
23+
}
24+
```
25+
26+
Response:
27+
28+
```json
29+
{
30+
"session_id": "$0",
31+
"session_name": "dev",
32+
"window_count": 3,
33+
"session_attached": "1",
34+
"session_created": "1713600000",
35+
"active_pane_id": "%0"
36+
}
37+
```
38+
39+
Resolve by name when only the session_name is known:
40+
41+
```json
42+
{
43+
"tool": "get_session_info",
44+
"arguments": {
45+
"session_name": "dev"
46+
}
47+
}
48+
```
49+
50+
```{fastmcp-tool-input} session_tools.get_session_info
51+
```

docs/tools/session/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Session-scoped tools — enumerate windows, rename or kill a session, switch win
99
Enumerate windows inside a session.
1010
:::
1111

12+
:::{grid-item-card} {tooliconl}`get-session-info`
13+
Read metadata for one session.
14+
:::
15+
1216
:::{grid-item-card} {tooliconl}`select-window`
1317
Switch to a window by id, index, or direction.
1418
:::
@@ -32,6 +36,7 @@ Terminate a session. Destructive.
3236
:maxdepth: 1
3337
3438
list-windows
39+
get-session-info
3540
select-window
3641
create-window
3742
rename-session

0 commit comments

Comments
 (0)