Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
_Notes on upcoming releases will be added here_
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### What's new

**Incremental pane observation with {tooliconl}`capture-since`**

{tooliconl}`capture-since` gives agents a cursor-based way to observe a pane without re-reading the same terminal output on every turn. The first call returns the current visible screen and an opaque cursor; later calls return only rows written or rewritten after that cursor while tmux still retains the needed history. If scrollback was cleared or trimmed, the result sets `lines_missed=True`, returns a conservative current visible capture, and issues a fresh cursor. Malformed cursors, cross-pane replay, pane death, and pane respawn fail clearly instead of silently switching processes. (#60)

## libtmux-mcp 0.1.0a9 (2026-05-24)

libtmux-mcp 0.1.0a9 tightens pane polling correctness for agents waiting on terminal output. Search and wait tools now handle wrapped content, history-limit risk reporting, and pane lifecycle changes with clearer results instead of silent false positives.
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Give your AI agent hands inside the terminal — create sessions, run commands,
| **Server** | `list_sessions`, `create_session`, `kill_server`, `get_server_info` |
| **Session** | `list_windows`, `get_session_info`, `create_window`, `rename_session`, `select_window`, `kill_session` |
| **Window** | `list_panes`, `get_window_info`, `split_window`, `rename_window`, `select_layout`, `resize_window`, `move_window`, `kill_window` |
| **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` |
| **Pane** | `send_keys`, `paste_text`, `capture_pane`, `capture_since`, `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` |
| **Options** | `show_option`, `set_option` |
| **Environment** | `show_environment`, `set_environment` |

Expand Down Expand Up @@ -99,6 +99,11 @@ returns content, cursor, copy-mode state, and scroll offset as one
typed value. The alternative is several `tmux` invocations stitched
together with regex.

**Observing.** [`capture_since`](https://libtmux-mcp.git-pull.com/tools/pane/capture-since/)
returns a cursor with the current pane content, then returns only
newly written or rewritten rows on follow-up calls. The alternative is
re-sending the same scrollback to the model on every check.

**Guarding.** The server detects the agent's own pane across sockets
and declines self-destructive operations — [`kill_session`](https://libtmux-mcp.git-pull.com/tools/session/kill-session/)
on itself fails loudly instead of silently terminating the host
Expand Down
16 changes: 8 additions & 8 deletions docs/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,31 @@ Standalone badges via `{badge}`:

### `{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`
{tool}`capture-pane` · {tool}`capture-since` · {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`
{toolref}`capture-pane` · {toolref}`capture-since` · {toolref}`send-keys` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`kill-pane` · {toolref}`create-session` · {toolref}`split-window`

### `{tooliconl}` — icon left, outside code

{tooliconl}`capture-pane` · {tooliconl}`send-keys` · {tooliconl}`search-panes` · {tooliconl}`wait-for-text` · {tooliconl}`kill-pane` · {tooliconl}`create-session` · {tooliconl}`split-window`
{tooliconl}`capture-pane` · {tooliconl}`capture-since` · {tooliconl}`send-keys` · {tooliconl}`search-panes` · {tooliconl}`wait-for-text` · {tooliconl}`kill-pane` · {tooliconl}`create-session` · {tooliconl}`split-window`

### `{tooliconr}` — icon right, outside code

{tooliconr}`capture-pane` · {tooliconr}`send-keys` · {tooliconr}`search-panes` · {tooliconr}`wait-for-text` · {tooliconr}`kill-pane` · {tooliconr}`create-session` · {tooliconr}`split-window`
{tooliconr}`capture-pane` · {tooliconr}`capture-since` · {tooliconr}`send-keys` · {tooliconr}`search-panes` · {tooliconr}`wait-for-text` · {tooliconr}`kill-pane` · {tooliconr}`create-session` · {tooliconr}`split-window`

### `{tooliconil}` — icon inline-left, inside code

{tooliconil}`capture-pane` · {tooliconil}`send-keys` · {tooliconil}`search-panes` · {tooliconil}`wait-for-text` · {tooliconil}`kill-pane` · {tooliconil}`create-session` · {tooliconil}`split-window`
{tooliconil}`capture-pane` · {tooliconil}`capture-since` · {tooliconil}`send-keys` · {tooliconil}`search-panes` · {tooliconil}`wait-for-text` · {tooliconil}`kill-pane` · {tooliconil}`create-session` · {tooliconil}`split-window`

### `{tooliconir}` — icon inline-right, inside code

{tooliconir}`capture-pane` · {tooliconir}`send-keys` · {tooliconir}`search-panes` · {tooliconir}`wait-for-text` · {tooliconir}`kill-pane` · {tooliconir}`create-session` · {tooliconir}`split-window`
{tooliconir}`capture-pane` · {tooliconir}`capture-since` · {tooliconir}`send-keys` · {tooliconir}`search-panes` · {tooliconir}`wait-for-text` · {tooliconir}`kill-pane` · {tooliconir}`create-session` · {tooliconir}`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`
{ref}`capture-pane` · {ref}`capture-since` · {ref}`send-keys` · {ref}`search-panes` · {ref}`wait-for-text` · {ref}`kill-pane` · {ref}`create-session` · {ref}`split-window`

## Badges in context

Expand All @@ -66,7 +66,7 @@ These are the actual tool headings as they render on tool pages:

### In prose

Use {tooliconl}`search-panes` to find text across all panes. If you know which pane, use {tooliconl}`capture-pane` instead. After running a command with {tooliconl}`send-keys`, compose `tmux wait-for -S` and call {tooliconl}`wait-for-channel` before capturing.
Use {tooliconl}`search-panes` to find text across all panes. If you know which pane, use {tooliconl}`capture-pane` for one read or {tooliconl}`capture-since` for repeated observation. After running a command with {tooliconl}`send-keys`, compose `tmux wait-for -S` and call {tooliconl}`wait-for-channel` before capturing.

### Dense inline (toolref, no badges)

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Config blocks for Claude Desktop, Claude Code, Cursor, and others.

Read tmux state without changing anything.

{toolref}`list-sessions` · {toolref}`capture-pane` · {toolref}`snapshot-pane` · {toolref}`get-pane-info` · {toolref}`find-pane-by-position` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`wait-for-content-change` · {toolref}`display-message`
{toolref}`list-sessions` · {toolref}`capture-pane` · {toolref}`capture-since` · {toolref}`snapshot-pane` · {toolref}`get-pane-info` · {toolref}`find-pane-by-position` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`wait-for-content-change` · {toolref}`display-message`

### Act (mutating)

Expand Down
11 changes: 8 additions & 3 deletions docs/prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ prefers {tool}`snapshot-pane`, which returns content + cursor
position + pane mode + scroll state in one call — saving a
follow-up ``get_pane_info`` round-trip. It also explicitly forbids
the agent from acting before it has a hypothesis, which prevents
"fix the symptom" anti-patterns.
"fix the symptom" anti-patterns. For repeated observation, it routes
follow-up reads through {tool}`capture-since` cursors instead of full
pane captures.

```{fastmcp-prompt-input} diagnose_failing_pane
```
Expand All @@ -124,9 +126,12 @@ Something went wrong in tmux pane %1. Diagnose it:
1. Call `snapshot_pane(pane_id="%1")` to get content,
cursor position, pane mode, and scroll state in one call.
2. If the content looks truncated, re-call with `max_lines=None`.
3. Identify the last command that ran (look at the prompt line and
3. If you need to watch the pane across more than one turn, call
`capture_since(pane_id="%1")`, keep the returned cursor,
and pass it to later `capture_since(cursor=...)` calls.
4. Identify the last command that ran (look at the prompt line and
the line above it) and the last non-empty output line.
4. Propose a root cause hypothesis and a minimal command to verify
5. Propose a root cause hypothesis and a minimal command to verify
it (do NOT execute anything yet — produce the plan first).
```

Expand Down
4 changes: 4 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ When you say "run `make test` and show me the output", the agent executes a thre

This **send → wait → capture** sequence is the fundamental workflow. For commands the agent authors, the channel pattern is deterministic; for output the agent does not author (third-party log lines, daemon prompts, interactive supervisors), substitute {tool}`wait-for-text` for step 2.

When you need to keep checking the same pane after that first read, switch to
{tool}`capture-since`: the first call returns a cursor, and follow-up calls
return only new pane output.

## Next steps

- {ref}`concepts` — Understand the tmux hierarchy and how tools target panes
Expand Down
7 changes: 3 additions & 4 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,9 @@ the ones above), use {toolref}`wait-for-text` instead.

{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).
{toolref}`capture-pane` calls or hardcoding a sleep duration. When the job is
already running and the agent needs to keep observing it across turns, use
{toolref}`capture-since` so each read returns only new pane output.

---

Expand Down Expand Up @@ -483,4 +483,3 @@ wait instead of polling, content vs. metadata, prefer IDs, escalate
gracefully -- see the {ref}`prompting guide <prompting>`. For specific
pitfalls like `enter: false` and the `send_keys`/`capture_pane` race
condition, see {ref}`gotchas <gotchas>`.

9 changes: 8 additions & 1 deletion docs/tools/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ All tools accept an optional `socket_name` parameter for multi-server support. I
## Which tool do I want?

**Reading terminal content?**
- Know which pane? → {tool}`capture-pane`
- Re-reading the same pane while it changes? → {tool}`capture-since`
- Need a one-shot read of a known pane? → {tool}`capture-pane`
- Need text + cursor + mode in one call? → {tool}`snapshot-pane`
- Don't know which pane? → {tool}`search-panes`
- Need to wait for specific output? → {tool}`wait-for-text`
Expand Down Expand Up @@ -99,6 +100,12 @@ List panes in a window.
Read visible content of a pane.
:::

:::{grid-item-card} capture_since
:link: capture-since
:link-type: ref
Start a cursor, then read only new pane output.
:::

:::{grid-item-card} get_pane_info
:link: get-pane-info
:link-type: ref
Expand Down
5 changes: 3 additions & 2 deletions docs/tools/pane/capture-pane.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
after running a command, checking output, or verifying state.

**Avoid when** you need to search across multiple panes at once — use
{tooliconl}`search-panes`. If you only need pane metadata (not content), use
{tooliconl}`get-pane-info`.
{tooliconl}`search-panes`. If you are repeatedly watching the same pane, use
{tooliconl}`capture-since` with its cursor so unchanged scrollback is not sent
again. If you only need pane metadata (not content), use {tooliconl}`get-pane-info`.

**Side effects:** None. Readonly.

Expand Down
92 changes: 92 additions & 0 deletions docs/tools/pane/capture-since.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Capture since

```{fastmcp-tool} pane_tools.capture_since
```

**Use when** you need to observe the same pane repeatedly — tailing logs,
watching a long-running command, checking a daemon, or revisiting a terminal
without paying to re-read the same scrollback every turn. The first call returns
the current visible screen plus a cursor; later calls pass that cursor back and
receive only rows written or rewritten after it.

**Avoid when** you control the command and only need completion — compose
`tmux wait-for -S <channel>` into the command and call
{tooliconl}`wait-for-channel`. If you need a one-shot content + metadata view,
use {tooliconl}`snapshot-pane`; if you do not know which pane contains text,
use {tooliconl}`search-panes`.

**Side effects:** None. Readonly.

**Example:**

Start a cursor with the currently visible screen:

```json
{
"tool": "capture_since",
"arguments": {
"pane_id": "%2"
}
}
```

Response:

```json
{
"pane_id": "%2",
"cursor": "capture-since-v1:...",
"lines": [
"$ pytest -vv",
"tests/test_api.py::test_health PASSED"
],
"elapsed_seconds": 0.003,
"lines_missed": false,
"truncated": false,
"truncated_lines": 0,
"truncated_bytes": 0
}
```

Read only content since that cursor:

```json
{
"tool": "capture_since",
"arguments": {
"cursor": "capture-since-v1:..."
}
}
```

The cursor carries the original pane id, so the follow-up call does not need
`pane_id`. If you pass both, they must match; a cursor for another pane raises a
tool error instead of silently reading the wrong process.

If nothing new was written after the cursor, `lines` is empty and the response
still includes a fresh cursor for the same pane. If the cursor row scrolled into
retained history, the tool can still return an exact delta; retained scrollback
is not a loss condition.

`lines_missed` becomes `true` when tmux has cleared or trimmed the history
needed to compute an exact delta. In that case, `lines` is a conservative
current visible capture and the response includes a fresh cursor.

Pane lifecycle is part of the cursor contract. If the pane dies or is respawned,
the call raises a tool error instead of reading from a different process that
reused the same pane id.

`truncated`, `truncated_lines`, and `truncated_bytes` are structured metadata.
No truncation marker is injected into `lines`, so clients can display terminal
text without parsing an in-band header.

The cursor is intentionally opaque. It is based on tmux grid state
(`history_size + cursor_y`) and pane lifecycle fields (`pane_id`, `pane_pid`);
see tmux's grid and capture implementation in
[grid.c](https://github.com/tmux/tmux/blob/134ba6c/grid.c) and
[cmd-capture-pane.c](https://github.com/tmux/tmux/blob/134ba6c/cmd-capture-pane.c),
and libtmux's
[`Pane.capture_pane()`](https://github.com/tmux-python/libtmux/blob/v0.58.0/src/libtmux/pane.py).

```{fastmcp-tool-input} pane_tools.capture_since
```
5 changes: 5 additions & 0 deletions docs/tools/pane/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Pane-scoped tools — read and drive individual terminals, wait for output, copy
Read visible or scrollback text from a pane.
:::

:::{grid-item-card} {tooliconl}`capture-since`
Start a cursor, then read only new pane output.
:::

:::{grid-item-card} {tooliconl}`search-panes`
Search text across many panes in one call.
:::
Expand Down Expand Up @@ -100,6 +104,7 @@ Terminate a pane. Destructive.
:maxdepth: 1

capture-pane
capture-since
search-panes
snapshot-pane
get-pane-info
Expand Down
2 changes: 1 addition & 1 deletion docs/tools/pane/search-panes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ which pane has an error, finding a running process, or checking output
without knowing which pane to look in.

**Avoid when** you already know the target pane — use {tooliconl}`capture-pane`
directly.
for a one-shot read, or {tooliconl}`capture-since` for repeated observation.

**Side effects:** None. Readonly.

Expand Down
6 changes: 3 additions & 3 deletions docs/tools/pane/wait-for-text.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
server to start, a build to complete, or a prompt to return.

**Avoid when** the expected text may never appear — always set a reasonable
`timeout`. For known output, {tooliconl}`capture-pane` after a known delay
may suffice, but `wait_for_text` is preferred because it adapts to variable
timing.
`timeout`. For repeated observation or tailing, use
{tooliconl}`capture-since`; for command completion you control, use
{tooliconl}`wait-for-channel`.

**Side effects:** None. Readonly. Blocks until text appears or timeout.

Expand Down
2 changes: 1 addition & 1 deletion docs/topics/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ src/libtmux_mcp/
server_tools.py # list_sessions, create_session, kill_server, get_server_info
session_tools.py # list_windows, create_window, rename_session, kill_session
window_tools.py # list_panes, split_window, rename_window, kill_window, select_layout, resize_window
pane_tools.py # send_keys, capture_pane, resize_pane, kill_pane, set_pane_title, get_pane_info, clear_pane, search_panes, wait_for_text
pane_tools.py # send_keys, capture_pane, capture_since, resize_pane, kill_pane, set_pane_title, get_pane_info, clear_pane, search_panes, wait_for_text
option_tools.py # show_option, set_option
env_tools.py # show_environment, set_environment
resources/
Expand Down
2 changes: 1 addition & 1 deletion docs/topics/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ For pane tools, you can combine parameters to narrow the search: `session_name`

Tools fall into three categories:

- **Discovery** — Read-only operations: `list_sessions`, `list_windows`, `list_panes`, `capture_pane`, `get_pane_info`, `find_pane_by_position`, `search_panes`, `wait_for_text`, `show_option`, `show_environment`
- **Discovery** — Read-only operations: `list_sessions`, `list_windows`, `list_panes`, `capture_pane`, `capture_since`, `get_pane_info`, `find_pane_by_position`, `search_panes`, `wait_for_text`, `show_option`, `show_environment`
- **Mutation** — Create, modify, or send input: `create_session`, `create_window`, `split_window`, `send_keys`, `rename_*`, `resize_*`, `set_pane_title`, `clear_pane`, `select_layout`, `set_option`, `set_environment`
- **Destruction** — Remove tmux objects: `kill_server`, `kill_session`, `kill_window`, `kill_pane`

Expand Down
11 changes: 10 additions & 1 deletion docs/topics/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Things that will bite you if you don't know about them in advance. For symptom-b

{tooliconl}`list-panes` and {tooliconl}`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 {tooliconl}`search-panes`. To read what a specific pane shows, use {tooliconl}`capture-pane`.
To find text that is visible in terminals, use {tooliconl}`search-panes`. To read what a specific pane shows once, use {tooliconl}`capture-pane`; to keep watching that pane, use {tooliconl}`capture-since`.

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`.

Expand Down Expand Up @@ -41,6 +41,15 @@ The capture above may return the terminal state **before** pytest runs. Compose

For output the agent does not author (third-party logs, daemon prompts, interactive supervisors), substitute {tooliconl}`wait-for-text` for `wait_for_channel`. See {ref}`recipes` for the complete pattern.

## Repeated `capture_pane` calls resend old output

If you are tailing a pane or checking a long-running process over several
turns, repeated {tooliconl}`capture-pane` calls keep returning the same visible
screen and scrollback. Use {tooliconl}`capture-since` instead: the first call
returns a cursor, and follow-up calls return only output written or rewritten
after that cursor. If tmux has already trimmed or cleared the needed history,
the result marks `lines_missed=true` and gives you a fresh cursor.

## 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`).
Expand Down
4 changes: 2 additions & 2 deletions docs/topics/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ hierarchy.
:::{grid-item-card} Pagination
:link: pagination
:link-type: doc
Protocol-level cursors vs tool-level ``offset`` / ``limit`` (as in
``search_panes``).
Protocol cursors, ``search_panes`` paging, and ``capture_since``
observation cursors.
:::

::::
Expand Down
Loading