豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Add respawn_pane + get_{session,window}_info, refactor docs extension imports#27

Draft
tony wants to merge 11 commits intomainfrom
2026-04-follow-ups-2
Draft

Add respawn_pane + get_{session,window}_info, refactor docs extension imports#27
tony wants to merge 11 commits intomainfrom
2026-04-follow-ups-2

Conversation

@tony
Copy link
Copy Markdown
Member

@tony tony commented Apr 20, 2026

Summary

  • Add respawn_pane: restart a pane's process in place (tmux respawn-pane -k), preserving pane_id and layout — the right primitive for agent shell-recovery flows that would otherwise kill_pane + split_window and invalidate held references.
  • Add get_session_info / get_window_info: single-object metadata reads that complete the core-hierarchy symmetry alongside existing get_pane_info / get_server_info. Callers with an ID no longer need list_* + filter.
  • Sharpen LLM-facing discoverability on display_message (retitled "Evaluate tmux Format String", docstring now leads with what it actually does — read-only format expansion, not a UI message) and pipe_pane (docstring leads with the concrete /tmp/pane.log logging use case).
  • Expand _BASE_INSTRUCTIONS with three disambiguators: hooks are read-only by design, the load/paste/delete_buffer lifecycle (and why there is no list_buffers), and the is_caller=true workflow for "which pane am I in?" — heads off exploratory calls for tools that don't exist.
  • Refactor the bundled Sphinx extension: import path changes from widgets to docs._ext.widgets, removing a mypy mypy_path alias hack that was needed only to paper over the dual-name resolution.
  • Add scripts/mcp_swap.py: dev tool that detects which agent CLIs (Claude, Codex, Cursor, Gemini) are installed and swaps their MCP config to run the local checkout editable-style (or reverts to the pre-swap state from timestamped backups). Motivated by "does this branch work on all four CLIs?" — previously a four-config hand-edit dance; now just mcp-use-local / just mcp-revert.

Changes by area

New tools (src/libtmux_mcp/tools/)

  • pane_tools/lifecycle.py: respawn_pane(pane_id, session_name, session_id, window_id, kill=True, shell_command, start_directory, socket_name) -> PaneInfo. kill=True threads -k (matches the recovery-flow intent); optional shell_command maps to the positional arg, start_directory maps to -c. Calls pane.refresh() so the returned PaneInfo carries the fresh pane_pid and pane_current_command. Registered with ANNOTATIONS_MUTATING / TAG_MUTATING.
  • session_tools.py: get_session_info(session_id, session_name, socket_name) -> SessionInfo. Reuses _resolve_session / _serialize_session — no new helpers. Registered with ANNOTATIONS_RO / TAG_READONLY.
  • window_tools.py: get_window_info(window_id, window_index, session_name, session_id, socket_name) -> WindowInfo. Reuses _resolve_window / _serialize_window. Inline comment memorializes the boundary: "the symmetry argument is bounded to the four-level core hierarchy — NOT a license to add get_buffer_info / get_hook_info / get_option_info".

Instruction / discoverability tweaks

  • src/libtmux_mcp/server.py (_BASE_INSTRUCTIONS + _build_instructions): add HOOKS ARE READ-ONLY block, BUFFERS block, and — inside the if tmux_pane: branch only — a sentence pointing agents at list_panes + is_caller=true filter instead of a nonexistent whoami tool. The workflow sentence is deliberately scoped to the in-tmux branch because it references "your pane is identified above", which is only true when the agent-context line has been emitted.
  • pane_tools/meta.py (display_message) + pane_tools/__init__.py: retitled to "Evaluate tmux Format String"; docstring now leads with "Evaluate a tmux format string against a target and return the expanded value" so the name-vs-behavior mismatch (display_message is the tmux verb, but -p expands rather than displays) stops biting LLM clients.
  • pane_tools/pipe.py (pipe_pane): summary reframed to "Log a pane's live output to a file" with an explicit /tmp/pane.log hint.

Docs extension refactor

Before After
docs/_ext/ on sys.path, imported as widgets docs/ on sys.path, imported as docs._ext.widgets
mypy_path = ["docs/_ext"] + exclude = ["^docs/"] juggling to avoid "source file found twice" exclude = ["^docs/"] alone — mypy_path alias removed
docs/_ext/__init__.py missing (implicit namespace) explicit package marker added
  • docs/conf.py: sys.path.insert(project_root) replaces sys.path.insert(_EXT_DIR); extension listed as docs._ext.widgets.
  • tests/docs/conftest.py + tests/docs/test_widgets.py: imports rewritten to from docs._ext.widgets import ... and the generated per-test conf.py follows suit.
  • docs/_widgets/mcp-install/widget.html: comment reference updated to docs._ext.widgets._base.make_highlight_filter.

Docs pages

  • docs/tools/pane/respawn-pane.md, docs/tools/session/get-session-info.md, docs/tools/window/get-window-info.md — each with a "Use when / Avoid when / Side effects" block, a primary example, and a resolve-by-alternate-key example where relevant.
  • Session, Window, and Pane index pages gain grid-item-cards and toctree entries for the new tools.
  • README.md: tool catalog rows updated for Session, Window, and Pane to list the three new tools.

Dev tooling: scripts/mcp_swap.py

A single-file PEP 723 script that centralizes the "point every installed CLI at my local checkout" flow. Four subcommands: detect (which CLIs exist), status (what each resolves libtmux to today), use-local (rewrite each config to uv --directory <repo> run libtmux-mcp), revert (restore from backups).

CLI Config path Format Server key
Claude ~/.claude.json JSON projects."<abs-repo>".mcpServers.libtmux
Codex ~/.codex/config.toml TOML [mcp_servers.libtmux]
Cursor ~/.cursor/mcp.json JSON mcpServers.libtmux
Gemini ~/.gemini/settings.json JSON mcpServers.libtmux
  • Server name + entry auto-derived from pyproject.toml: project.name with trailing -mcp stripped (libtmux-mcplibtmux), first [project.scripts] key for the entry command. Flags override (--server, --entry). Generalizes cleanly to any uv-managed MCP repo.
  • Format preservation: tomlkit for Codex's TOML so top-level comments, sibling tables, and key order survive the round-trip; stdlib json for the three JSON configs (none of them rely on comments).
  • Claude's per-project keying is handled explicitly — only the current repo's projects."<abs-repo>" block is rewritten; other projects' entries are untouched. Claude's full entry shape (type, env) is preserved even when empty.
  • Safety: timestamped .bak.mcp-swap-<ts> backups (never overwrites existing backups); atomic tempfile + pathlib.Path.replace; post-write re-parse validation with rollback on parse failure; --dry-run prints unified diffs and writes nothing; state file at ~/.local/state/libtmux-mcp/mcp_swap.json tracking per-CLI backup paths so revert handles the "added" case (Codex had no entry → revert removes the block, doesn't restore a phantom previous version).
  • Four [group: 'mcp'] just recipes: mcp-detect, mcp-status, mcp-use-local, mcp-revert.
  • scripts/README.md covers usage + extension (add an entry to CLIS and extend the three per-CLI branches in get_server / set_server / delete_server).
  • tomlkit>=0.13 added to the dev dependency group so pytest can import the PEP 723 script.

Design decisions

  • respawn_pane defaults kill=True: the tool exists for recovery flows where the process is already wedged. Defaulting to kill=False would surface "pane is not dead" errors as the common case and push every caller to set the flag manually.
  • -e env not exposed on respawn_pane: set_environment already covers that path; keeping the surface small avoids two ways to do the same thing.
  • respawn_window deferred: audit §6.1 — ship respawn_pane first, add the window variant only if usage demands it.
  • get_*_info bounded to the four-level hierarchy: server / session / window / pane. Buffers, hooks, and options have existing show_* / load_* reads; adding get_buffer_info etc. would bloat the surface without adding capability. Inline comment on get_window_info documents this so future contributors don't relitigate.
  • is_caller workflow sentence lives inside _build_instructions, not _BASE_INSTRUCTIONS: the sentence references "your pane is identified above", which is only true when TMUX_PANE is set. Putting it in the base string would be a lie outside tmux.
  • Docs extension as a real package: the mypy_path alias was a hack — mypy saw the widget modules under two names (widgets and docs._ext.widgets) and needed an exclude to avoid "source file found twice" during full-tree traversal. Making docs/_ext/__init__.py explicit and importing consistently as docs._ext.widgets collapses the two paths into one.
  • mcp_swap.py lives in scripts/, not as an installed entry point: it's developer infrastructure, not product. Keeping it a PEP 723 single file means uv run scripts/mcp_swap.py works with zero setup, and copying the file to another MCP repo is a one-command adoption path.
  • mcp_swap.py replaces rather than sidecars the libtmux entry: one libtmux key per CLI, not libtmux-dev + libtmux. This avoids two entries both claiming the same tool namespace and matches the user's manual "swap" flow the .bak.libtmux-dev backup files hinted at.

Verification

Verify the new tools are registered:

rg -n "respawn_pane|get_session_info|get_window_info" src/libtmux_mcp/tools/

Verify the widget import refactor left no widgets top-level imports:

rg -n "^from widgets|^import widgets" src tests docs

Verify mypy_path alias is gone:

rg -n "mypy_path" pyproject.toml

Verify instruction blocks are present:

rg -n "HOOKS ARE READ-ONLY|BUFFERS:|whoami tool" src/libtmux_mcp/server.py

End-to-end test the swap script (reversible round-trip):

just mcp-detect
just mcp-status
just mcp-use-local --dry-run
just mcp-use-local
just mcp-revert

Test plan

  • test_respawn_pane_preserves_pane_id_and_refreshes_pid — pane_id survives, pane_pid changes
  • test_respawn_pane_replaces_shell_commandshell_command override takes effect
  • test_get_session_info + test_get_session_info_by_name — resolves by id and by name
  • test_get_window_info + test_get_window_info_by_index — resolves by id and by index+session
  • test_base_instructions_document_hook_boundaryHOOKS ARE READ-ONLY + show_hooks + "tmux config file" present
  • test_base_instructions_document_buffer_lifecycle — load/paste/delete/BufferRef/list_buffers/clipboard history all present
  • test_build_instructions_documents_is_caller_workflow_inside_tmux — workflow sentence present with TMUX_PANE set, absent without
  • Widget tests import via docs._ext.widgets and the generated test conf.py picks them up
  • tests/test_mcp_swap.py — 12 cases: JSON round-trip (cursor/gemini), Claude per-project keying, Codex comment preservation + add-when-missing + revert-removes, unrelated-server preservation, --dry-run writes nothing, second-swap-is-noop, state-file cleared on full revert, McpServerSpec shape helpers
  • End-to-end swap cycle against real configs: mcp-use-local → backups written → all four CLIs report local → mcp-revert → configs byte-identical to pre-swap backups (verified via cmp) → mcp-use-local again → libtmux entries byte-identical to the prior local state (Claude's whole file hash drifts because Claude Code autosaves unrelated fields; the libtmux entry itself is stable)
  • uv run mypy . — succeeds without the mypy_path alias (strict, 53 files)
  • uv run ruff check . — clean
  • just test / uv run pytest — full suite green (387 tests)

tony added 10 commits April 20, 2026 17:00
why: Establish a real package boundary under docs._ext before repointing imports.
what:
- add docs/_ext/__init__.py
- make the docs extension namespace importable as a package root
why: Use one canonical module path for the docs widget extension so tools and tests resolve it consistently.
what:
- load the Sphinx extension via docs._ext.widgets
- put the repo root on sys.path for docs and widget tests
- update widget test imports to the canonical package path
why: The docs widgets no longer need a top-level alias, and keeping one reintroduces duplicate module identities.
what:
- remove docs/_ext from mypy_path
- keep the existing checked roots and docs exclusion unchanged
why: The template comment should point at the canonical docs._ext.widgets module path.
what:
- update the widget.html comment to reference docs._ext.widgets._base
…rs to _BASE_INSTRUCTIONS

why: agents waste turns asking for write-hook tools (not exposed by
design), expecting a list_buffers affordance (privacy-contract
declined), or reinventing a whoami tool (three layers already answer
"where am I?"). Naming each boundary explicitly heads off the
exploratory call.

what:
- Append HOOKS ARE READ-ONLY paragraph to _BASE_INSTRUCTIONS citing
  show_hooks / show_hook and pointing at the tmux config file for
  writes.
- Append BUFFERS paragraph covering load_buffer/paste_buffer/
  delete_buffer lifecycle, BufferRef tracking, and the OS-clipboard
  privacy reason for the list_buffers omission.
- Extend the TMUX_PANE branch of _build_instructions with a one-
  sentence is_caller workflow pointer — only emitted when running
  inside tmux, since the sentence references the agent-context line.
- Lock all three additions with new assertion tests.
…iscoverability

why: the name 'display_message' reads to an LLM like "show a
notification to the user" — the opposite of what the tool does
(evaluate a tmux format string and return the expanded value).
_BASE_INSTRUCTIONS already had corrective prose to compensate, which
is structural evidence the name is doing the wrong job. Rewording the
docstring summary and the MCP title lets the description carry the
meaning without touching the wire-name. A hard rename to
evaluate_format is deliberately deferred behind telemetry.

what:
- Rewrite the first sentence of display_message's docstring so
  FastMCP-indexed description leads with 'Evaluate a tmux format
  string... and return the expanded value.' — FastMCP pulls
  description from the docstring when no description= kwarg is given
  (fastmcp/tools/function_tool.py:225-227).
- Change the mcp.tool registration title from 'Display Message' to
  'Evaluate tmux Format String' in pane_tools/__init__.py.
- Refresh the corrective sentence in _BASE_INSTRUCTIONS to match the
  new wording and name-check the title shift.
- Complete the truncated '## Act' section in display-message.md and
  lead with the new framing; retitle the page to reflect the MCP
  title.
…case

why: 'Start or stop piping pane output to a file.' is accurate but
doesn't give the agent the hook it needs to reach for this tool.
FastMCP indexes the docstring summary for search and LLM clients feed
it into system prompts — a concrete example ('cat > /tmp/pane.log')
anchors the discoverability without changing any behavior.

what:
- Replace the flat two-line summary with 'Log a pane's live output to
  a file (or stop an active log)' plus a one-sentence typical-use
  hint. The semantic reframe (log, not pipe) matches how agents
  think about the affordance.
why: get_pane_info and get_server_info exist, but the window and
session peers do not. Agents given a window_id and asked "what are
this window's dimensions?" have to call list_panes or list_windows
and filter — wasteful. Adding get_window_info closes one half of the
core-tmux-hierarchy symmetry (get_session_info follows next).

The symmetry argument is deliberately bounded to the four-level core
hierarchy (Server > Session > Window > Pane). This is NOT a license
to add get_buffer_info / get_hook_info / get_option_info — those
scopes are outside the hierarchy and the existing show_*/load_*
tools already cover their reads. An inline comment on the function
memorializes that boundary so future contributors don't re-relitigate
it.

what:
- Add get_window_info(window_id, window_index, session_name,
  session_id, socket_name) returning WindowInfo. Reuses
  _resolve_window (accepts window_id OR window_index+session) and
  _serialize_window — no new helpers.
- Register with ANNOTATIONS_RO + TAG_READONLY, placed next to
  list_panes in the Window tool group.
- Add test_get_window_info (resolves by window_id) and
  test_get_window_info_by_index (resolves by index+session) mirroring
  the minimal-assertion style used by test_list_panes.
- Add docs/tools/window/get-window-info.md modeled after
  get-pane-info.md; insert the new page into the Window tools index
  grid and toctree.
- Append get_window_info to the README tool catalog Window row.
…adata

why: completes the symmetry started by get_window_info. Agents given
a session_id and asked "how many windows does this session have?" no
longer need to call list_sessions and filter.

The bounded-to-core-hierarchy rule from get_window_info applies here
too — inline comment memorializes it.

what:
- Add get_session_info(session_id, session_name, socket_name)
  returning SessionInfo. Reuses _resolve_session and
  _serialize_session; no new helpers.
- Register with ANNOTATIONS_RO + TAG_READONLY, placed next to
  list_windows in the Session tool group.
- Add test_get_session_info (by id) and test_get_session_info_by_name
  mirroring the test_list_windows style.
- Add docs/tools/session/get-session-info.md modeled after the peer
  get-window-info page; insert the new page into the Session index
  grid and toctree.
- Append get_session_info to the README tool catalog Session row.
why: when an agent wedges a shell (hung REPL, runaway process, bad
terminal mode) the only current recourse is kill_pane + split_window,
which destroys pane_id references the agent may still be holding and
reshuffles the layout. tmux's respawn-pane -k restarts the process
in place, preserving both pane_id and layout — the right primitive
for agent recovery flows.

what:
- Add respawn_pane(pane_id, session_name, session_id, window_id,
  kill=True, shell_command, start_directory, socket_name) returning
  PaneInfo. Default kill=True threads -k to tmux (matches the
  recovery-flow intent). Optional shell_command and start_directory
  map to tmux's respawn-pane positional arg and -c flag respectively;
  -e env is deliberately omitted (use set_environment if needed).
- Call pane.refresh() after the cmd so _serialize_pane reads the
  fresh pane_pid and pane_current_command.
- Register with ANNOTATIONS_MUTATING + TAG_MUTATING, placed next to
  kill_pane in the pane lifecycle module. Export from pane_tools
  __init__.
- Add two tests: test_respawn_pane_preserves_pane_id_and_refreshes_pid
  (pane_id survives, pane_pid changes) and
  test_respawn_pane_replaces_shell_command (shell_command override
  takes effect).
- Add docs/tools/pane/respawn-pane.md modeled on kill-pane.md plus
  relaunch-with-different-command example; add the page to the Pane
  tools index grid and toctree.
- Append respawn_pane to the README tool catalog Pane row.

Defer respawn_window until respawn_pane shows usage (audit §6.1).
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 20, 2026

Codecov Report

❌ Patch coverage is 67.12707% with 119 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.29%. Comparing base (c0c7bf7) to head (b987006).

Files with missing lines Patch % Lines
scripts/mcp_swap.py 66.46% 92 Missing and 18 partials ⚠️
src/libtmux_mcp/tools/pane_tools/lifecycle.py 61.11% 4 Missing and 3 partials ⚠️
tests/docs/conftest.py 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #27      +/-   ##
==========================================
- Coverage   86.67%   83.29%   -3.39%     
==========================================
  Files          38       39       +1     
  Lines        1719     2077     +358     
  Branches      204      261      +57     
==========================================
+ Hits         1490     1730     +240     
- Misses        169      266      +97     
- Partials       60       81      +21     

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

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

@tony tony changed the title Add respawn_pane + get_{session,window}_info, refactor docs extension imports Add respawn_pane + get_{session,window}_info, refactor docs extension imports Apr 20, 2026
…tool

why: when developing an MCP server locally, each agent CLI (Claude, Codex,
Cursor, Gemini) needs its config pointed at the checkout so uv run picks
up source edits editable-style. Doing that by hand across four config
formats (three JSON shapes + TOML) is fragile — Codex's TOML in particular
needs comment/order preservation that sed/jq can't offer. A scripted swap
with timestamped backups + a state file makes the flow reversible and
reproducible across any MCP repo, not just this one.

what:
- Add scripts/mcp_swap.py as a PEP 723 single-file script modeled on
  ~/scripts/py/agentex_mcp.py: inline uv deps (tomlkit>=0.13),
  CLIName = Literal["claude","codex","cursor","gemini"], per-CLI
  get/set/delete on the MCP server entry. Subcommands: detect, status,
  use-local, revert. Server name + entry command auto-derived from the
  repo's pyproject.toml (libtmux-mcp -> libtmux, first [project.scripts]
  key for the entry).
- Claude's per-project keying (projects."<abs-repo>".mcpServers) is
  handled explicitly — only the current repo's key is rewritten, leaving
  other projects untouched. Claude's full entry shape is preserved
  (type/env fields).
- Codex TOML edits go through tomlkit so [notice] blocks, top-level
  comments, and sibling tables survive the round-trip. When no
  libtmux entry exists yet (common for Codex), use-local records
  action="added" so revert correctly removes the block.
- Safety: atomic tempfile+os.replace writes, timestamped
  .bak.mcp-swap-<ts> backups (never overwrites existing .bak.*), post-
  write re-parse validation with rollback on failure, --dry-run that
  prints unified diffs and writes nothing, state file at
  ~/.local/state/libtmux-mcp/mcp_swap.json tracking per-CLI backup paths.
- Add scripts/README.md with usage + extension notes (add an entry to
  CLIS and extend the three per-CLI branches).
- Add 12 tests in tests/test_mcp_swap.py using importlib.util to load
  the out-of-tree script: JSON round-trip (cursor/gemini), Claude
  per-project keying, Codex comment preservation + add-then-revert,
  unrelated-server preservation, --dry-run writes nothing,
  second-swap-is-noop, state-file cleared on full revert, spec helpers.
- Add four [group: 'mcp'] recipes to justfile: mcp-detect, mcp-status,
  mcp-use-local, mcp-revert.
- Add tomlkit>=0.13 to the dev dependency group so pytest can import
  the PEP 723 script without uv run bootstrapping.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants