Add respawn_pane + get_{session,window}_info, refactor docs extension imports#27
Draft
Add respawn_pane + get_{session,window}_info, refactor docs extension imports#27
respawn_pane + get_{session,window}_info, refactor docs extension imports#27Conversation
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 Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
respawn_pane + get_{session,window}_info, refactor docs extension imports
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
respawn_pane: restart a pane's process in place (tmuxrespawn-pane -k), preservingpane_idand layout — the right primitive for agent shell-recovery flows that would otherwisekill_pane+split_windowand invalidate held references.get_session_info/get_window_info: single-object metadata reads that complete the core-hierarchy symmetry alongside existingget_pane_info/get_server_info. Callers with an ID no longer needlist_*+ filter.display_message(retitled "Evaluate tmux Format String", docstring now leads with what it actually does — read-only format expansion, not a UI message) andpipe_pane(docstring leads with the concrete/tmp/pane.loglogging use case)._BASE_INSTRUCTIONSwith three disambiguators: hooks are read-only by design, theload/paste/delete_bufferlifecycle (and why there is nolist_buffers), and theis_caller=trueworkflow for "which pane am I in?" — heads off exploratory calls for tools that don't exist.widgetstodocs._ext.widgets, removing a mypymypy_pathalias hack that was needed only to paper over the dual-name resolution.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; nowjust 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=Truethreads-k(matches the recovery-flow intent); optionalshell_commandmaps to the positional arg,start_directorymaps to-c. Callspane.refresh()so the returnedPaneInfocarries the freshpane_pidandpane_current_command. Registered withANNOTATIONS_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 withANNOTATIONS_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 addget_buffer_info/get_hook_info/get_option_info".Instruction / discoverability tweaks
src/libtmux_mcp/server.py(_BASE_INSTRUCTIONS+_build_instructions): addHOOKS ARE READ-ONLYblock,BUFFERSblock, and — inside theif tmux_pane:branch only — a sentence pointing agents atlist_panes+is_caller=truefilter instead of a nonexistentwhoamitool. 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_messageis the tmux verb, but-pexpands 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.loghint.Docs extension refactor
docs/_ext/onsys.path, imported aswidgetsdocs/onsys.path, imported asdocs._ext.widgetsmypy_path = ["docs/_ext"]+exclude = ["^docs/"]juggling to avoid "source file found twice"exclude = ["^docs/"]alone —mypy_pathalias removeddocs/_ext/__init__.pymissing (implicit namespace)docs/conf.py:sys.path.insert(project_root)replacessys.path.insert(_EXT_DIR); extension listed asdocs._ext.widgets.tests/docs/conftest.py+tests/docs/test_widgets.py: imports rewritten tofrom docs._ext.widgets import ...and the generated per-testconf.pyfollows suit.docs/_widgets/mcp-install/widget.html: comment reference updated todocs._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.README.md: tool catalog rows updated for Session, Window, and Pane to list the three new tools.Dev tooling:
scripts/mcp_swap.pyA 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 touv --directory <repo> run libtmux-mcp),revert(restore from backups).~/.claude.jsonprojects."<abs-repo>".mcpServers.libtmux~/.codex/config.toml[mcp_servers.libtmux]~/.cursor/mcp.jsonmcpServers.libtmux~/.gemini/settings.jsonmcpServers.libtmuxpyproject.toml:project.namewith trailing-mcpstripped (libtmux-mcp→libtmux), first[project.scripts]key for the entry command. Flags override (--server,--entry). Generalizes cleanly to any uv-managed MCP repo.tomlkitfor Codex's TOML so top-level comments, sibling tables, and key order survive the round-trip; stdlibjsonfor the three JSON configs (none of them rely on comments).projects."<abs-repo>"block is rewritten; other projects' entries are untouched. Claude's full entry shape (type,env) is preserved even when empty..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-runprints unified diffs and writes nothing; state file at~/.local/state/libtmux-mcp/mcp_swap.jsontracking per-CLI backup paths soreverthandles the "added" case (Codex had no entry →revertremoves the block, doesn't restore a phantom previous version).[group: 'mcp']just recipes:mcp-detect,mcp-status,mcp-use-local,mcp-revert.scripts/README.mdcovers usage + extension (add an entry toCLISand extend the three per-CLI branches inget_server/set_server/delete_server).tomlkit>=0.13added to the dev dependency group so pytest can import the PEP 723 script.Design decisions
respawn_panedefaultskill=True: the tool exists for recovery flows where the process is already wedged. Defaulting tokill=Falsewould surface "pane is not dead" errors as the common case and push every caller to set the flag manually.-eenv not exposed onrespawn_pane:set_environmentalready covers that path; keeping the surface small avoids two ways to do the same thing.respawn_windowdeferred: audit §6.1 — shiprespawn_panefirst, add the window variant only if usage demands it.get_*_infobounded to the four-level hierarchy: server / session / window / pane. Buffers, hooks, and options have existingshow_*/load_*reads; addingget_buffer_infoetc. would bloat the surface without adding capability. Inline comment onget_window_infodocuments this so future contributors don't relitigate.is_callerworkflow sentence lives inside_build_instructions, not_BASE_INSTRUCTIONS: the sentence references "your pane is identified above", which is only true whenTMUX_PANEis set. Putting it in the base string would be a lie outside tmux.mypy_pathalias was a hack — mypy saw the widget modules under two names (widgetsanddocs._ext.widgets) and needed anexcludeto avoid "source file found twice" during full-tree traversal. Makingdocs/_ext/__init__.pyexplicit and importing consistently asdocs._ext.widgetscollapses the two paths into one.mcp_swap.pylives inscripts/, not as an installed entry point: it's developer infrastructure, not product. Keeping it a PEP 723 single file meansuv run scripts/mcp_swap.pyworks with zero setup, and copying the file to another MCP repo is a one-command adoption path.mcp_swap.pyreplaces rather than sidecars the libtmux entry: onelibtmuxkey per CLI, notlibtmux-dev+libtmux. This avoids two entries both claiming the same tool namespace and matches the user's manual "swap" flow the.bak.libtmux-devbackup files hinted at.Verification
Verify the new tools are registered:
Verify the widget import refactor left no
widgetstop-level imports:Verify
mypy_pathalias is gone:Verify instruction blocks are present:
End-to-end test the swap script (reversible round-trip):
Test plan
test_respawn_pane_preserves_pane_id_and_refreshes_pid— pane_id survives, pane_pid changestest_respawn_pane_replaces_shell_command—shell_commandoverride takes effecttest_get_session_info+test_get_session_info_by_name— resolves by id and by nametest_get_window_info+test_get_window_info_by_index— resolves by id and by index+sessiontest_base_instructions_document_hook_boundary—HOOKS ARE READ-ONLY+show_hooks+ "tmux config file" presenttest_base_instructions_document_buffer_lifecycle— load/paste/delete/BufferRef/list_buffers/clipboard history all presenttest_build_instructions_documents_is_caller_workflow_inside_tmux— workflow sentence present withTMUX_PANEset, absent withoutdocs._ext.widgetsand the generated testconf.pypicks them uptests/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-runwrites nothing, second-swap-is-noop, state-file cleared on full revert,McpServerSpecshape helpersmcp-use-local→ backups written → all four CLIs report local →mcp-revert→ configs byte-identical to pre-swap backups (verified viacmp) →mcp-use-localagain → 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 themypy_pathalias (strict, 53 files)uv run ruff check .— cleanjust test/uv run pytest— full suite green (387 tests)